diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 26145d7ce7..f717e49cbf 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "3.0.0-release.214", + "version": "3.0.0-release.216", "downloadFileNames": { "Windows_86": "win-x86-net6.0.zip", "Windows_64": "win-x64-net6.0.zip", diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 3bce6e4fcc..29dd5f175c 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -1061,6 +1061,14 @@ declare module 'azdata' { */ export type DesignerEditPath = (string | number)[]; + /** + * Severity of the messages returned by the provider after processing an edit. + * 'error': The issue must be fixed in order to commit the changes. + * 'warning': Inform the user the potential risks with the current state. e.g. Having multiple edge constraints is only useful as a temporary state. + * 'information': Informational message. + */ + export type DesignerIssueSeverity = 'error' | 'warning' | 'information'; + /** * The result returned by the table designer provider after handling an edit request. */ @@ -1078,9 +1086,9 @@ declare module 'azdata' { */ isValid: boolean; /** - * Error messages of current state, and the property the caused the error. + * Issues of current state. */ - errors?: { message: string, propertyPath?: DesignerEditPath }[]; + issues?: { severity: DesignerIssueSeverity, description: string, propertyPath?: DesignerEditPath }[]; } /** diff --git a/src/sql/workbench/browser/designer/designer.ts b/src/sql/workbench/browser/designer/designer.ts index e11207a818..242917ac99 100644 --- a/src/sql/workbench/browser/designer/designer.ts +++ b/src/sql/workbench/browser/designer/designer.ts @@ -36,7 +36,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { DesignerMessagesTabPanelView } from 'sql/workbench/browser/designer/designerMessagesTabPanelView'; +import { DesignerIssuesTabPanelView } from 'sql/workbench/browser/designer/designerIssuesTabPanelView'; import { DesignerScriptEditorTabPanelView } from 'sql/workbench/browser/designer/designerScriptEditorTabPanelView'; import { DesignerPropertyPathValidator } from 'sql/workbench/browser/designer/designerPropertyPathValidator'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -70,7 +70,7 @@ interface DesignerTableCellContext { } const ScriptTabId = 'scripts'; -const MessagesTabId = 'messages'; +const IssuesTabId = 'issues'; export class Designer extends Disposable implements IThemable { private _loadingSpinner: LoadingSpinner; @@ -95,7 +95,7 @@ export class Designer extends Disposable implements IThemable { private _inputDisposable: DisposableStore; private _loadingTimeoutHandle: any; private _groupHeaders: HTMLElement[] = []; - private _messagesView: DesignerMessagesTabPanelView; + private _issuesView: DesignerIssuesTabPanelView; private _scriptEditorView: DesignerScriptEditorTabPanelView; private _onStyleChangeEventEmitter = new Emitter(); @@ -152,8 +152,8 @@ export class Designer extends Disposable implements IThemable { onDidChange: Event.None }, Sizing.Distribute); this._scriptTabbedPannel = new TabbedPanel(this._editorContainer); - this._messagesView = this._instantiationService.createInstance(DesignerMessagesTabPanelView); - this._register(this._messagesView.onMessageSelected((path) => { + this._issuesView = this._instantiationService.createInstance(DesignerIssuesTabPanelView); + this._register(this._issuesView.onIssueSelected((path) => { if (path && path.length > 0) { this.selectProperty(path); } @@ -332,14 +332,14 @@ export class Designer extends Disposable implements IThemable { private handleEditProcessedEvent(args: DesignerEditProcessedEventArgs): void { const edit = args.edit; this._supressEditProcessing = true; - if (!args.result.isValid) { - alert(localize('designer.errorCountAlert', "{0} validation errors found.", args.result.errors.length)); + if (args.result.issues?.length > 0) { + alert(localize('designer.issueCountAlert', "{0} validation issues found.", args.result.issues.length)); } try { if (args.result.refreshView) { this.refresh(); if (!args.result.isValid) { - this._scriptTabbedPannel.showTab(MessagesTabId); + this._scriptTabbedPannel.showTab(IssuesTabId); } } else { this.updateComponentValues(); @@ -466,7 +466,7 @@ export class Designer extends Disposable implements IThemable { } private updateComponentValues(): void { - this.updateMessagesTab(); + this.updateIssuesTab(); const viewModel = this._input.viewModel; const scriptProperty = viewModel[ScriptProperty] as InputBoxProperties; if (scriptProperty) { @@ -477,23 +477,24 @@ export class Designer extends Disposable implements IThemable { }); } - private updateMessagesTab(): void { + private updateIssuesTab(): void { if (!this._input) { return; } - if (this._scriptTabbedPannel.contains(MessagesTabId)) { - this._scriptTabbedPannel.removeTab(MessagesTabId); + if (this._scriptTabbedPannel.contains(IssuesTabId)) { + this._scriptTabbedPannel.removeTab(IssuesTabId); } - if (this._input.validationErrors === undefined || this._input.validationErrors.length === 0) { + + if (this._input.issues === undefined || this._input.issues.length === 0) { return; } this._scriptTabbedPannel.pushTab({ - title: localize('designer.messagesTabTitle', "Errors ({0})", this._input.validationErrors.length), - identifier: MessagesTabId, - view: this._messagesView + title: localize('designer.issuesTabTitle', "Issues ({0})", this._input.issues.length), + identifier: IssuesTabId, + view: this._issuesView }); - this._scriptTabbedPannel.showTab(MessagesTabId); - this._messagesView.updateMessages(this._input.validationErrors); + this._scriptTabbedPannel.showTab(IssuesTabId); + this._issuesView.updateIssues(this._input.issues); } private selectProperty(path: DesignerPropertyPath, view?: DesignerUIArea, highlight: boolean = true): void { diff --git a/src/sql/workbench/browser/designer/designerIssuesTabPanelView.ts b/src/sql/workbench/browser/designer/designerIssuesTabPanelView.ts new file mode 100644 index 0000000000..9693f362cb --- /dev/null +++ b/src/sql/workbench/browser/designer/designerIssuesTabPanelView.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPanelView } from 'sql/base/browser/ui/panel/panel'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as DOM from 'vs/base/browser/dom'; +import { DesignerPropertyPath, DesignerIssue } from 'sql/workbench/browser/designer/interfaces'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IListAccessibilityProvider, List } from 'vs/base/browser/ui/list/listWidget'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { localize } from 'vs/nls'; +import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { attachListStyler } from 'vs/platform/theme/common/styler'; +import { problemsErrorIconForeground, problemsInfoIconForeground, problemsWarningIconForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Codicon } from 'vs/base/common/codicons'; + +export class DesignerIssuesTabPanelView extends Disposable implements IPanelView { + private _container: HTMLElement; + private _onIssueSelected = new Emitter(); + private _issueList: List; + + public readonly onIssueSelected: Event = this._onIssueSelected.event; + + constructor(@IThemeService private _themeService: IThemeService) { + super(); + } + + render(container: HTMLElement): void { + this._container = container.appendChild(DOM.$('.issues-container')); + this._issueList = new List('designerIssueList', this._container, new DesignerIssueListDelegate(), [new TableFilterListRenderer()], { + multipleSelectionSupport: false, + keyboardSupport: true, + mouseSupport: true, + accessibilityProvider: new DesignerIssueListAccessibilityProvider() + }); + this._register(this._issueList.onDidChangeSelection((e) => { + if (e.elements && e.elements.length === 1) { + this._onIssueSelected.fire(e.elements[0].propertyPath); + } + })); + this._register(attachListStyler(this._issueList, this._themeService)); + } + + layout(dimension: DOM.Dimension): void { + this._issueList.layout(dimension.height, dimension.width); + } + + updateIssues(errors: DesignerIssue[]) { + if (this._issueList) { + this._issueList.splice(0, this._issueList.length, errors); + } + } +} + +registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const errorForegroundColor = theme.getColor(problemsErrorIconForeground); + const warningForegroundColor = theme.getColor(problemsWarningIconForeground); + const informationalForegroundColor = theme.getColor(problemsInfoIconForeground); + if (errorForegroundColor) { + collector.addRule(` + .designer-component .issues-container .issue-item .issue-icon.codicon-error { + color: ${errorForegroundColor}; + } + .designer-component .issues-container .issue-item .issue-icon.codicon-warning { + color: ${warningForegroundColor}; + } + .designer-component .issues-container .issue-item .issue-icon.codicon-info { + color: ${informationalForegroundColor}; + } + `); + } +}); + +const DesignerIssueListTemplateId = 'DesignerIssueListTemplate'; +class DesignerIssueListDelegate implements IListVirtualDelegate { + getHeight(element: DesignerIssue): number { + return 25; + } + + getTemplateId(element: DesignerIssue): string { + return DesignerIssueListTemplateId; + } +} + +interface DesignerIssueListItemTemplate { + issueText: HTMLDivElement; + issueIcon: HTMLDivElement; +} + +class TableFilterListRenderer implements IListRenderer { + renderTemplate(container: HTMLElement): DesignerIssueListItemTemplate { + const data: DesignerIssueListItemTemplate = Object.create(null); + const issueItem = container.appendChild(DOM.$('.issue-item')); + data.issueIcon = issueItem.appendChild(DOM.$('')); + data.issueText = issueItem.appendChild(DOM.$('.issue-text')); + return data; + } + + renderElement(element: DesignerIssue, index: number, templateData: DesignerIssueListItemTemplate, height: number): void { + templateData.issueText.innerText = element.description; + templateData.issueText.title = element.description; + let iconClass; + switch (element.severity) { + case 'warning': + iconClass = Codicon.warning.classNames; + break; + case 'information': + iconClass = Codicon.info.classNames; + break; + default: + iconClass = Codicon.error.classNames; + break; + } + templateData.issueIcon.className = `issue-icon ${iconClass}`; + } + + public disposeTemplate(templateData: DesignerIssueListItemTemplate): void { + } + + public get templateId(): string { + return DesignerIssueListTemplateId; + } +} + +class DesignerIssueListAccessibilityProvider implements IListAccessibilityProvider { + getAriaLabel(element: DesignerIssue): string { + return element.description; + } + + getWidgetAriaLabel(): string { + return localize('designer.IssueListAriaLabel', "Issues"); + } + + getWidgetRole() { + return 'listbox'; + } + + getRole(element: DesignerIssue): string { + return 'option'; + } +} diff --git a/src/sql/workbench/browser/designer/designerMessagesTabPanelView.ts b/src/sql/workbench/browser/designer/designerMessagesTabPanelView.ts deleted file mode 100644 index 27909c4566..0000000000 --- a/src/sql/workbench/browser/designer/designerMessagesTabPanelView.ts +++ /dev/null @@ -1,123 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IPanelView } from 'sql/base/browser/ui/panel/panel'; -import { Disposable } from 'vs/base/common/lifecycle'; -import * as DOM from 'vs/base/browser/dom'; -import { DesignerPropertyPath, DesignerValidationError } from 'sql/workbench/browser/designer/interfaces'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IListAccessibilityProvider, List } from 'vs/base/browser/ui/list/listWidget'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { localize } from 'vs/nls'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { attachListStyler } from 'vs/platform/theme/common/styler'; -import { problemsErrorIconForeground } from 'vs/platform/theme/common/colorRegistry'; -import { Codicon } from 'vs/base/common/codicons'; - -export class DesignerMessagesTabPanelView extends Disposable implements IPanelView { - private _container: HTMLElement; - private _onMessageSelected = new Emitter(); - private _messageList: List; - - public readonly onMessageSelected: Event = this._onMessageSelected.event; - - constructor(@IThemeService private _themeService: IThemeService) { - super(); - } - - render(container: HTMLElement): void { - this._container = container.appendChild(DOM.$('.messages-container')); - this._messageList = new List('designerMessageList', this._container, new DesignerMessageListDelegate(), [new TableFilterListRenderer()], { - multipleSelectionSupport: false, - keyboardSupport: true, - mouseSupport: true, - accessibilityProvider: new DesignerMessagesListAccessibilityProvider() - }); - this._register(this._messageList.onDidChangeSelection((e) => { - if (e.elements && e.elements.length === 1) { - this._onMessageSelected.fire(e.elements[0].propertyPath); - } - })); - this._register(attachListStyler(this._messageList, this._themeService)); - } - - layout(dimension: DOM.Dimension): void { - this._messageList.layout(dimension.height, dimension.width); - } - - updateMessages(errors: DesignerValidationError[]) { - if (this._messageList) { - this._messageList.splice(0, this._messageList.length, errors); - } - } -} - -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const errorForegroundColor = theme.getColor(problemsErrorIconForeground); - if (errorForegroundColor) { - collector.addRule(` - .designer-component .messages-container .message-item .message-icon { - color: ${errorForegroundColor}; - } - `); - } -}); - -const DesignerMessageListTemplateId = 'DesignerMessageListTemplate'; -class DesignerMessageListDelegate implements IListVirtualDelegate { - getHeight(element: DesignerValidationError): number { - return 25; - } - - getTemplateId(element: DesignerValidationError): string { - return DesignerMessageListTemplateId; - } -} - -interface DesignerMessageListItemTemplate { - messageText: HTMLDivElement; -} - -class TableFilterListRenderer implements IListRenderer { - renderTemplate(container: HTMLElement): DesignerMessageListItemTemplate { - const data: DesignerMessageListItemTemplate = Object.create(null); - const messageItem = container.appendChild(DOM.$('.message-item')); - messageItem.appendChild(DOM.$(`.message-icon${Codicon.error.cssSelector}`)); - data.messageText = messageItem.appendChild(DOM.$('.message-text')); - return data; - } - - renderElement(element: DesignerValidationError, index: number, templateData: DesignerMessageListItemTemplate, height: number): void { - templateData.messageText.innerText = element.message; - } - - disposeElement?(element: DesignerValidationError, index: number, templateData: DesignerMessageListItemTemplate, height: number): void { - } - - public disposeTemplate(templateData: DesignerMessageListItemTemplate): void { - } - - public get templateId(): string { - return DesignerMessageListTemplateId; - } -} - -class DesignerMessagesListAccessibilityProvider implements IListAccessibilityProvider { - getAriaLabel(element: DesignerValidationError): string { - return element.message; - } - - getWidgetAriaLabel(): string { - return localize('designer.MessageListAriaLabel', "Errors"); - } - - getWidgetRole() { - return 'listbox'; - } - - getRole(element: DesignerValidationError): string { - return 'option'; - } -} diff --git a/src/sql/workbench/browser/designer/interfaces.ts b/src/sql/workbench/browser/designer/interfaces.ts index cffe47181a..eb9099e79e 100644 --- a/src/sql/workbench/browser/designer/interfaces.ts +++ b/src/sql/workbench/browser/designer/interfaces.ts @@ -44,9 +44,9 @@ export interface DesignerComponentInput { readonly viewModel: DesignerViewModel; /** - * Gets the validation errors. + * Gets the issues. */ - readonly validationErrors: DesignerValidationError[] | undefined; + readonly issues: DesignerIssue[] | undefined; /** * Start initilizing the designer input object. @@ -229,12 +229,13 @@ export type DesignerUIArea = 'PropertiesView' | 'ScriptView' | 'TopContentView' export type DesignerPropertyPath = (string | number)[]; export const DesignerRootObjectPath: DesignerPropertyPath = []; -export type DesignerValidationError = { message: string, propertyPath?: DesignerPropertyPath }; +export type DesignerIssueSeverity = 'error' | 'warning' | 'information'; +export type DesignerIssue = { description: string, propertyPath?: DesignerPropertyPath, severity: DesignerIssueSeverity }; export interface DesignerEditResult { isValid: boolean; refreshView?: boolean; - errors?: DesignerValidationError[]; + issues?: DesignerIssue[]; } export interface DesignerTextEditor { diff --git a/src/sql/workbench/browser/designer/media/designer.css b/src/sql/workbench/browser/designer/media/designer.css index 3cc203ddbb..80e95f3d4d 100644 --- a/src/sql/workbench/browser/designer/media/designer.css +++ b/src/sql/workbench/browser/designer/media/designer.css @@ -29,24 +29,27 @@ height: 100%; } -.designer-component .messages-container { +.designer-component .issues-container { overflow: hidden; height: 100%; width: 100%; } -.designer-component .messages-container .message-item { +.designer-component .issues-container .issue-item { display: flex; } -.designer-component .messages-container .message-item .message-icon { +.designer-component .issues-container .issue-item .issue-icon { margin: 0px 6px; flex: 0 0 auto; line-height: 25px; } -.designer-component .messages-container .message-item .message-text { +.designer-component .issues-container .issue-item .issue-text { flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + user-select: text; } .designer-component .tabbed-panel-container { diff --git a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts index 05cc3e8913..7396ee489a 100644 --- a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts +++ b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerValidationError, ScriptProperty } from 'sql/workbench/browser/designer/interfaces'; +import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerIssue, ScriptProperty } from 'sql/workbench/browser/designer/interfaces'; import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface'; import { localize } from 'vs/nls'; import { designers } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -22,7 +22,7 @@ const ErrorDialogTitle: string = localize('tableDesigner.ErrorDialogTitle', "Tab export class TableDesignerComponentInput implements DesignerComponentInput { private _viewModel: DesignerViewModel; - private _validationErrors?: DesignerValidationError[]; + private _issues?: DesignerIssue[]; private _view: DesignerView; private _valid: boolean = true; private _dirty: boolean = false; @@ -76,8 +76,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput { return this._viewModel; } - get validationErrors(): DesignerValidationError[] | undefined { - return this._validationErrors; + get issues(): DesignerIssue[] | undefined { + return this._issues; } processEdit(edit: DesignerEdit): void { @@ -93,14 +93,14 @@ export class TableDesignerComponentInput implements DesignerComponentInput { if (result.view) { this.setDesignerView(result.view); } - this._validationErrors = result.errors; + this._issues = result.issues; this.updateState(result.isValid, this.isDirty(), undefined); this._onEditProcessed.fire({ edit: edit, result: { isValid: result.isValid, - errors: result.errors, + issues: result.issues, refreshView: !!result.view } }); @@ -372,7 +372,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput { description: localize('designer.column.description.precision', "For numeric data, the maximum number of decimal digits that can be stored in this database object."), componentProperties: { title: localize('tableDesigner.columnPrecisionTitle', "Precision"), - width: 60 + width: 60, + inputType: 'number' } }, { componentType: 'input', @@ -380,7 +381,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput { description: localize('designer.column.description.scale', "For numeric data, the maximum number of decimal digits that can be stored in this database object to the right of decimal point."), componentProperties: { title: localize('tableDesigner.columnScaleTitle', "Scale"), - width: 60 + width: 60, + inputType: 'number' } } ];