diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 1e3699c798..050993dcad 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "4.0.0.5", + "version": "4.0.0.9", "downloadFileNames": { "Windows_86": "win-x86-net6.0.zip", "Windows_64": "win-x64-net6.0.zip", diff --git a/extensions/mssql/src/tableDesigner/tableDesigner.ts b/extensions/mssql/src/tableDesigner/tableDesigner.ts index fdd573db33..7d5fa92e23 100644 --- a/extensions/mssql/src/tableDesigner/tableDesigner.ts +++ b/extensions/mssql/src/tableDesigner/tableDesigner.ts @@ -9,13 +9,18 @@ import * as vscode from 'vscode'; import { sqlProviderName } from '../constants'; import { generateUuid } from 'vscode-languageclient/lib/utils/uuid'; import { ITelemetryEventProperties, Telemetry } from '../telemetry'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); +const NewTableText = localize('tableDesigner.NewTable', "New Table"); export function registerTableDesignerCommands(appContext: AppContext) { appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.newTable', async (context: azdata.ObjectExplorerContext) => { const connectionString = await azdata.connection.getConnectionString(context.connectionProfile.id, true); const tableIcon = context.nodeInfo.nodeSubType as azdata.designers.TableIcon; const telemetryInfo = await getTelemetryInfo(context, tableIcon); await azdata.designers.openTableDesigner(sqlProviderName, { + title: NewTableText, + tooltip: `${context.connectionProfile.serverName} - ${context.connectionProfile.databaseName} - ${NewTableText}`, server: context.connectionProfile.serverName, database: context.connectionProfile.databaseName, isNewTable: true, @@ -35,6 +40,8 @@ export function registerTableDesignerCommands(appContext: AppContext) { const tableIcon = context.nodeInfo.nodeSubType as azdata.designers.TableIcon; const telemetryInfo = await getTelemetryInfo(context, tableIcon); await azdata.designers.openTableDesigner(sqlProviderName, { + title: `${schema}.${name}`, + tooltip: `${server} - ${database} - ${schema}.${name}`, server: server, database: database, isNewTable: false, diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 919ba2d2bd..b8d31c7122 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -190,6 +190,11 @@ "command": "sqlDatabaseProjects.convertToSdkStyleProject", "title": "%sqlDatabaseProjects.convertToSdkStyleProject%", "category": "%sqlDatabaseProjects.displayName%" + }, + { + "command": "sqlDatabaseProjects.openInDesigner", + "title": "%sqlDatabaseProjects.openInDesigner%", + "category": "%sqlDatabaseProjects.displayName%" } ], "menus": { @@ -305,6 +310,10 @@ { "command": "sqlDatabaseProjects.convertToSdkStyleProject", "when": "false" + }, + { + "command": "sqlDatabaseProjects.openInDesigner", + "when": "false" } ], "view/item/context": [ @@ -388,6 +397,11 @@ "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.file.externalStreamingJob", "group": "5_dbProjects_streamingJob" }, + { + "command": "sqlDatabaseProjects.openInDesigner", + "when": "azdataAvailable && view == dataworkspace.views.main && viewItem == databaseProject.itemType.file.table && config.workbench.enablePreviewFeatures", + "group": "6_dbProjects_openInDesigner" + }, { "command": "sqlDatabaseProjects.exclude", "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.folder || viewItem =~ /^databaseProject.itemType.file/", diff --git a/extensions/sql-database-projects/package.nls.json b/extensions/sql-database-projects/package.nls.json index 851ddbc606..262a62f3a3 100644 --- a/extensions/sql-database-projects/package.nls.json +++ b/extensions/sql-database-projects/package.nls.json @@ -33,6 +33,7 @@ "sqlDatabaseProjects.changeTargetPlatform": "Change Target Platform", "sqlDatabaseProjects.generateProjectFromOpenApiSpec": "Generate SQL Project from OpenAPI/Swagger spec", "sqlDatabaseProjects.convertToSdkStyleProject": "Convert to SDK-style project", + "sqlDatabaseProjects.openInDesigner": "Open in Designer", "sqlDatabaseProjects.Settings": "Database Projects", "sqlDatabaseProjects.dotnetInstallLocation": "Full path to .NET SDK on the machine.", diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index e2db944f7b..4053b19b0d 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -529,6 +529,7 @@ export enum DatabaseProjectItemType { folder = 'databaseProject.itemType.folder', file = 'databaseProject.itemType.file', externalStreamingJob = 'databaseProject.itemType.file.externalStreamingJob', + table = 'databaseProject.itemType.file.table', referencesRoot = 'databaseProject.itemType.referencesRoot', reference = 'databaseProject.itemType.reference', dataSourceRoot = 'databaseProject.itemType.dataSourceRoot', diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 11c3d2e071..2b13c175fd 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -17,6 +17,9 @@ import { WorkspaceTreeItem } from 'dataworkspace'; import * as constants from '../common/constants'; import { SqlDatabaseProjectProvider } from '../projectProvider/projectProvider'; import { GenerateProjectFromOpenApiSpecOptions, ItemType } from 'sqldbproj'; +import { TableFileNode } from '../models/tree/fileFolderTreeItem'; +import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; +import { getAzdataApi } from '../common/utils'; /** * The main controller class that initializes the extension @@ -56,35 +59,56 @@ export default class MainController implements vscode.Disposable { private async initializeDatabaseProjects(): Promise { // init commands - vscode.commands.registerCommand('sqlDatabaseProjects.properties', async (node: WorkspaceTreeItem) => { return vscode.window.showErrorMessage(`Properties not yet implemented: ${node.element.uri.path}`); }); // TODO + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.properties', async (node: WorkspaceTreeItem) => { return vscode.window.showErrorMessage(`Properties not yet implemented: ${node.element.uri.path}`); })); // TODO - vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: WorkspaceTreeItem) => { return this.projectsController.buildProject(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: WorkspaceTreeItem) => { return this.projectsController.publishProject(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: WorkspaceTreeItem) => { return this.projectsController.schemaCompare(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.schemaComparePublishProjectChanges', async (operationId: string, projectFilePath: string, folderStructure: string): Promise => { return await this.projectsController.schemaComparePublishProjectChanges(operationId, projectFilePath, folderStructure); }); - vscode.commands.registerCommand('sqlDatabaseProjects.updateProjectFromDatabase', async (node: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | WorkspaceTreeItem) => { await this.projectsController.updateProjectFromDatabase(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.createProjectFromDatabase', async (context: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | undefined) => { return this.projectsController.createProjectFromDatabase(context); }); - vscode.commands.registerCommand('sqlDatabaseProjects.generateProjectFromOpenApiSpec', async (options?: GenerateProjectFromOpenApiSpecOptions) => { return this.projectsController.generateProjectFromOpenApiSpec(options); }); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: WorkspaceTreeItem) => { return this.projectsController.buildProject(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: WorkspaceTreeItem) => { return this.projectsController.publishProject(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: WorkspaceTreeItem) => { return this.projectsController.schemaCompare(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.schemaComparePublishProjectChanges', async (operationId: string, projectFilePath: string, folderStructure: string): Promise => { return await this.projectsController.schemaComparePublishProjectChanges(operationId, projectFilePath, folderStructure); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.updateProjectFromDatabase', async (node: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | WorkspaceTreeItem) => { await this.projectsController.updateProjectFromDatabase(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.createProjectFromDatabase', async (context: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | undefined) => { return this.projectsController.createProjectFromDatabase(context); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.generateProjectFromOpenApiSpec', async (options?: GenerateProjectFromOpenApiSpecOptions) => { return this.projectsController.generateProjectFromOpenApiSpec(options); })); - vscode.commands.registerCommand('sqlDatabaseProjects.newScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.script); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newPreDeploymentScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.preDeployScript); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newPostDeploymentScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.postDeployScript); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newTable', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.table); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newView', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.view); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.storedProcedure); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newExternalStreamingJob', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.externalStreamingJob); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.addExistingItem', async (node: WorkspaceTreeItem) => { return this.projectsController.addExistingItemPrompt(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: WorkspaceTreeItem) => { return this.projectsController.addFolderPrompt(node); }); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.script); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newPreDeploymentScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.preDeployScript); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newPostDeploymentScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.postDeployScript); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newTable', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.table); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newView', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.view); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.storedProcedure); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newExternalStreamingJob', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, ItemType.externalStreamingJob); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.addExistingItem', async (node: WorkspaceTreeItem) => { return this.projectsController.addExistingItemPrompt(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: WorkspaceTreeItem) => { return this.projectsController.addFolderPrompt(node); })); - vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: WorkspaceTreeItem) => { return this.projectsController.addDatabaseReference(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: WorkspaceTreeItem) => { return this.projectsController.openContainingFolder(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.editProjectFile', async (node: WorkspaceTreeItem) => { return this.projectsController.editProjectFile(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.convertToSdkStyleProject', async (node: WorkspaceTreeItem) => { return this.projectsController.convertToSdkStyleProject(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: WorkspaceTreeItem) => { return this.projectsController.delete(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: WorkspaceTreeItem) => { return this.projectsController.exclude(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: WorkspaceTreeItem) => { return this.projectsController.changeTargetPlatform(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.validateExternalStreamingJob', async (node: WorkspaceTreeItem) => { return this.projectsController.validateExternalStreamingJob(node); }); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: WorkspaceTreeItem) => { return this.projectsController.addDatabaseReference(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: WorkspaceTreeItem) => { return this.projectsController.openContainingFolder(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.editProjectFile', async (node: WorkspaceTreeItem) => { return this.projectsController.editProjectFile(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.convertToSdkStyleProject', async (node: WorkspaceTreeItem) => { return this.projectsController.convertToSdkStyleProject(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: WorkspaceTreeItem) => { return this.projectsController.delete(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: WorkspaceTreeItem) => { return this.projectsController.exclude(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: WorkspaceTreeItem) => { return this.projectsController.changeTargetPlatform(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.validateExternalStreamingJob', async (node: WorkspaceTreeItem) => { return this.projectsController.validateExternalStreamingJob(node); })); + this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.openInDesigner', async (node: WorkspaceTreeItem) => { + if (node?.element instanceof TableFileNode) { + const tableFileNode = node.element as TableFileNode; + const projectNode = tableFileNode.root as ProjectRootTreeItem; + const filePath = tableFileNode.fileSystemUri.fsPath; + const projectPath = projectNode.project.projectFilePath; + const targetVersion = projectNode.project.getProjectTargetVersion(); + await getAzdataApi()!.designers.openTableDesigner('MSSQL', { + title: tableFileNode.friendlyName, + tooltip: `${projectPath} - ${tableFileNode.friendlyName}`, + id: filePath, + isNewTable: false, + tableScriptPath: filePath, + projectFilePath: projectPath, + allScripts: projectNode.project.files.map(entry => entry.fsUri.fsPath), + targetVersion: targetVersion + }, { + 'ProjectTargetVersion': targetVersion + }); + } + })); IconPathHelper.setExtensionContext(this.extensionContext); diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index f1935ce51a..f5cc126765 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -245,10 +245,20 @@ export class Project implements ISqlProject { // create a FileProjectEntry for each file const fileEntries: FileProjectEntry[] = []; - filesSet.forEach(f => { + for (let f of Array.from(filesSet.values())) { const typeEntry = entriesWithType.find(e => e.relativePath === f); - fileEntries.push(this.createFileProjectEntry(f, EntryType.File, typeEntry ? typeEntry.typeAttribute : undefined)); - }); + let containsCreateTableStatement; + + // read file to check if it has a "Create Table" statement + const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(f)); + + if (await utils.exists(fullPath)) { + const fileContents = await fs.readFile(fullPath); + containsCreateTableStatement = fileContents.toString().toLowerCase().includes('create table'); + } + + fileEntries.push(this.createFileProjectEntry(f, EntryType.File, typeEntry ? typeEntry.typeAttribute : undefined, containsCreateTableStatement)); + } return fileEntries; } @@ -1078,13 +1088,14 @@ export class Project implements ISqlProject { return this.getCollectionProjectPropertyValue(constants.DatabaseSource); } - public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string): FileProjectEntry { + public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string, containsCreateTableStatement?: boolean): FileProjectEntry { let platformSafeRelativePath = utils.getPlatformSafeFileEntryPath(relativePath); return new FileProjectEntry( Uri.file(path.join(this.projectFolderPath, platformSafeRelativePath)), utils.convertSlashesForSqlProj(relativePath), entryType, - sqlObjectType); + sqlObjectType, + containsCreateTableStatement); } private findOrCreateItemGroup(containedTag?: string, prePostScriptExist?: { scriptExist: boolean; }): Element { diff --git a/extensions/sql-database-projects/src/models/projectEntry.ts b/extensions/sql-database-projects/src/models/projectEntry.ts index 4ca70a1ff8..e70df625f0 100644 --- a/extensions/sql-database-projects/src/models/projectEntry.ts +++ b/extensions/sql-database-projects/src/models/projectEntry.ts @@ -27,12 +27,14 @@ export class FileProjectEntry extends ProjectEntry implements IFileProjectEntry fsUri: Uri; relativePath: string; sqlObjectType: string | undefined; + containsCreateTableStatement: boolean | undefined; - constructor(uri: Uri, relativePath: string, entryType: EntryType, sqlObjectType?: string) { + constructor(uri: Uri, relativePath: string, entryType: EntryType, sqlObjectType?: string, containsCreateTableStatement?: boolean) { super(entryType); this.fsUri = uri; this.relativePath = relativePath; this.sqlObjectType = sqlObjectType; + this.containsCreateTableStatement = containsCreateTableStatement; } public override toString(): string { diff --git a/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts b/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts index d785c7423e..847d2909e3 100644 --- a/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts @@ -80,6 +80,15 @@ export class ExternalStreamingJobFileNode extends FileNode { } } +export class TableFileNode extends FileNode { + public override get treeItem(): vscode.TreeItem { + const treeItem = super.treeItem; + treeItem.contextValue = DatabaseProjectItemType.table; + + return treeItem; + } +} + /** * Compares two folder/file tree nodes so that folders come before files, then alphabetically * @param a a folder or file tree node diff --git a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts index 88bb2c024c..bd10ba3545 100644 --- a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts @@ -80,6 +80,8 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem { case EntryType.File: if (entry.sqlObjectType === ExternalStreamingJob) { newNode = new fileTree.ExternalStreamingJobFileNode(entry.fsUri, parentNode); + } else if (entry.containsCreateTableStatement) { + newNode = new fileTree.TableFileNode(entry.fsUri, parentNode); } else { newNode = new fileTree.FileNode(entry.fsUri, parentNode); diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 7f29b6b802..802e472a9a 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -735,29 +735,21 @@ declare module 'azdata' { */ export interface TableInfo { /** - * The server name. + * Used as the table designer editor's tab header text. */ - server: string; + title: string; /** - * The database name + * Used as the table designer editor's tab header hover text. */ - database: string; - /** - * The schema name, only required for existing table. - */ - schema?: string; - /** - * The table name, only required for existing table. - */ - name?: string; - /** - * A boolean value indicates whether a new table is being designed. - */ - isNewTable: boolean; + tooltip: string; /** * Unique identifier of the table. Will be used to decide whether a designer is already opened for the table. */ id: string; + /** + * A boolean value indicates whether a new table is being designed. + */ + isNewTable: boolean; /** * Extension can store additional information that the provider needs to uniquely identify a table. */ @@ -781,6 +773,14 @@ declare module 'azdata' { * The initial state of the designer. */ viewModel: DesignerViewModel; + /** + * The new table info after initialization. + */ + tableInfo: TableInfo; + /** + * The issues. + */ + issues?: DesignerIssue[]; } /** @@ -933,6 +933,10 @@ declare module 'azdata' { * Additional primary key properties. Common primary key properties: primaryKeyName, primaryKeyDescription. */ additionalPrimaryKeyProperties?: DesignerDataPropertyInfo[]; + /** + * Whether to use advanced save mode. for advanced save mode, a publish changes dialog will be opened with preview of changes. + */ + useAdvancedSaveMode: boolean; } export interface TableDesignerBuiltInTableViewOptions extends DesignerTablePropertiesBase { @@ -1107,7 +1111,7 @@ declare module 'azdata' { /** * the path of the edit target. */ - path: DesignerEditPath; + path: DesignerPropertyPath; /** * the new value. */ @@ -1115,7 +1119,7 @@ declare module 'azdata' { } /** - * The path of the edit target. + * The path of the property. * Below are the 3 scenarios and their expected path. * Note: 'index-{x}' in the description below are numbers represent the index of the object in the list. * 1. 'Add' scenario @@ -1129,7 +1133,7 @@ declare module 'azdata' { * a. ['propertyName1',index-1]. Example: remove a column from the columns property: ['columns',0']. * b. ['propertyName1',index-1,'propertyName2',index-2]. Example: remove a column mapping from a foreign key's column mapping table: ['foreignKeys',0,'mappings',0]. */ - export type DesignerEditPath = (string | number)[]; + export type DesignerPropertyPath = (string | number)[]; /** * Severity of the messages returned by the provider after processing an edit. @@ -1139,6 +1143,28 @@ declare module 'azdata' { */ export type DesignerIssueSeverity = 'error' | 'warning' | 'information'; + /** + * Represents the issue in the designer + */ + export interface DesignerIssue { + /** + * Severity of the issue. + */ + severity: DesignerIssueSeverity, + /** + * Path of the property that is associated with the issue. + */ + propertyPath?: DesignerPropertyPath, + /** + * Description of the issue. + */ + description: string, + /** + * Url to a web page that has the explaination of the issue. + */ + moreInfoLink?: string; + } + /** * The result returned by the table designer provider after handling an edit request. */ @@ -1158,7 +1184,7 @@ declare module 'azdata' { /** * Issues of current state. */ - issues?: { severity: DesignerIssueSeverity, description: string, propertyPath?: DesignerEditPath }[]; + issues?: DesignerIssue[]; /** * The input validation error. */ diff --git a/src/sql/workbench/browser/designer/interfaces.ts b/src/sql/workbench/browser/designer/interfaces.ts index b5fd62ca9d..6c1526a67a 100644 --- a/src/sql/workbench/browser/designer/interfaces.ts +++ b/src/sql/workbench/browser/designer/interfaces.ts @@ -51,7 +51,7 @@ export interface DesignerComponentInput { /** * Start initilizing the designer input object. */ - initialize(): void; + initialize(): Promise; /** * Start processing the edit made in the designer, the OnEditProcessed event will be fired when the processing is done. diff --git a/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts b/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts index cfb9cd11a8..9fee4c212f 100644 --- a/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts +++ b/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts @@ -11,13 +11,10 @@ import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/comm import * as azdata from 'azdata'; import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Schemas } from 'sql/base/common/schemas'; import { INotificationService } from 'vs/platform/notification/common/notification'; -const NewTable: string = localize('tableDesigner.newTable', "New Table"); - enum TableIcon { Basic = 'Basic', Temporal = 'Temporal', @@ -43,19 +40,20 @@ export class TableDesignerInput extends EditorInput { tableInfo: azdata.designers.TableInfo, telemetryInfo: { [key: string]: string }, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IEditorService private readonly _editorService: IEditorService, @INotificationService private readonly _notificationService: INotificationService) { super(); this._designerComponentInput = this._instantiationService.createInstance(TableDesignerComponentInput, this._provider, tableInfo, telemetryInfo); this._register(this._designerComponentInput.onStateChange((e) => { if (e.previousState.pendingAction === 'publish') { this.setEditorLabel(); - this._onDidChangeLabel.fire(); } if (e.currentState.dirty !== e.previousState.dirty) { this._onDidChangeDirty.fire(); } })); + this._register(this._designerComponentInput.onInitialized(() => { + this.setEditorLabel(); + })); // default to basic if icon is null (new table) or no sub type this._tableIcon = tableInfo.tableIcon ? tableInfo.tableIcon as TableIcon : TableIcon.Basic; @@ -97,7 +95,7 @@ export class TableDesignerInput extends EditorInput { if (this._designerComponentInput.pendingAction) { this._notificationService.warn(localize('tableDesigner.OperationInProgressWarning', "The operation cannot be performed while another operation is in progress.")); } else { - await this._designerComponentInput.openPublishDialog(); + await this._designerComponentInput.save(); } return this; } @@ -118,18 +116,8 @@ export class TableDesignerInput extends EditorInput { } private setEditorLabel(): void { - const tableInfo = this._designerComponentInput.tableInfo; - if (tableInfo.isNewTable) { - const existingNames = this._editorService.editors.map(editor => editor.getName()); - // Find the next available unique name for the new table designer - let idx = 1; - do { - this._name = `${NewTable} ${idx}`; - idx++; - } while (existingNames.indexOf(this._name) !== -1); - } else { - this._name = `${tableInfo.schema}.${tableInfo.name}`; - } - this._title = `${tableInfo.server}.${tableInfo.database} - ${this._name}`; + this._name = this._designerComponentInput.tableInfo.title; + this._title = this._designerComponentInput.tableInfo.tooltip; + this._onDidChangeLabel.fire(); } } diff --git a/src/sql/workbench/contrib/tableDesigner/browser/actions.ts b/src/sql/workbench/contrib/tableDesigner/browser/actions.ts index 24f316f61f..497eec7dd5 100644 --- a/src/sql/workbench/contrib/tableDesigner/browser/actions.ts +++ b/src/sql/workbench/contrib/tableDesigner/browser/actions.ts @@ -6,57 +6,56 @@ import { TableDesignerComponentInput } from 'sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput'; import { Action } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -export abstract class TableChangesActionBase extends Action { - protected _input: TableDesignerComponentInput; - private _onStateChangeDisposable: IDisposable; +const PublishChangesLabel = localize('tableDesigner.publishTableChanges', "Publish Changes..."); +const SaveChangesLabel = localize('tableDesigner.saveTableChanges', "Save"); - constructor(id: string, label: string, iconClassNames: string) { - super(id, label, iconClassNames); +export class SaveTableChangesAction extends Action { + public static ID = 'tableDesigner.publishTableChanges'; + protected _input: TableDesignerComponentInput; + protected _inputDisposableStore: DisposableStore; + + constructor() { + super(SaveTableChangesAction.ID); + this._inputDisposableStore = new DisposableStore(); } public setContext(input: TableDesignerComponentInput): void { this._input = input; this.updateState(); - this._onStateChangeDisposable?.dispose(); - this._onStateChangeDisposable = input.onStateChange((e) => { + this.updateLabelAndIcon(); + this._inputDisposableStore?.dispose(); + this._inputDisposableStore = new DisposableStore(); + this._inputDisposableStore.add(input.onStateChange((e) => { this.updateState(); - }); + })); + this._inputDisposableStore.add(input.onInitialized(() => { + this.updateLabelAndIcon(); + })); } private updateState(): void { this.enabled = this._input.dirty && this._input.valid && this._input.pendingAction === undefined; } + private updateLabelAndIcon(): void { + if (this._input?.tableDesignerView?.useAdvancedSaveMode) { + this.label = PublishChangesLabel; + this.class = Codicon.repoPush.classNames; + } else { + this.label = SaveChangesLabel; + this.class = Codicon.save.classNames; + } + } + + public override async run(): Promise { + await this._input.save(); + } + override dispose() { super.dispose(); - this._onStateChangeDisposable?.dispose(); - } -} - -export class PublishTableChangesAction extends TableChangesActionBase { - public static ID = 'tableDesigner.publishTableChanges'; - public static LABEL = localize('tableDesigner.publishTableChanges', "Publish Changes..."); - constructor() { - super(PublishTableChangesAction.ID, PublishTableChangesAction.LABEL, Codicon.repoPush.classNames); - } - - public override async run(): Promise { - await this._input.openPublishDialog(); - } -} - -export class GenerateTableChangeScriptAction extends TableChangesActionBase { - public static ID = 'tableDesigner.generateScript'; - public static LABEL = localize('tableDesigner.generateScript', "Generate Script"); - - constructor() { - super(GenerateTableChangeScriptAction.ID, GenerateTableChangeScriptAction.LABEL, Codicon.output.classNames); - } - - public override async run(): Promise { - await this._input.generateScript(); + this._inputDisposableStore?.dispose(); } } diff --git a/src/sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor.ts b/src/sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor.ts index 03f8440bfd..d5cfeb3c14 100644 --- a/src/sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor.ts +++ b/src/sql/workbench/contrib/tableDesigner/browser/tableDesignerEditor.ts @@ -16,7 +16,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { PublishTableChangesAction } from 'sql/workbench/contrib/tableDesigner/browser/actions'; +import { SaveTableChangesAction } from 'sql/workbench/contrib/tableDesigner/browser/actions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { DesignerPaneSeparator } from 'sql/platform/theme/common/colorRegistry'; @@ -26,7 +26,7 @@ export class TableDesignerEditor extends EditorPane { public static readonly ID: string = 'workbench.editor.tableDesigner'; private _designer: Designer; - private _publishChangesAction: PublishTableChangesAction; + private _saveChangesAction: SaveTableChangesAction; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -45,7 +45,7 @@ export class TableDesignerEditor extends EditorPane { await super.setInput(input, options, context, token); const designerInput = input.getComponentInput(); this._designer.setInput(designerInput); - this._publishChangesAction.setContext(designerInput); + this._saveChangesAction.setContext(designerInput); } protected createEditor(parent: HTMLElement): void { @@ -58,9 +58,9 @@ export class TableDesignerEditor extends EditorPane { const designerContainer = container.appendChild(DOM.$('.designer-container')); const actionbar = new ActionBar(actionbarContainer); this._register(actionbar); - this._publishChangesAction = this._instantiationService.createInstance(PublishTableChangesAction); - this._publishChangesAction.enabled = false; - actionbar.push([this._publishChangesAction], { icon: true, label: false }); + this._saveChangesAction = this._instantiationService.createInstance(SaveTableChangesAction); + this._saveChangesAction.enabled = false; + actionbar.push([this._saveChangesAction], { icon: true, label: false }); this._designer = this._instantiationService.createInstance(Designer, designerContainer); this._register(attachDesignerStyler(this._designer, this.themeService)); diff --git a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts index ee33a4fb72..df4b9d4823 100644 --- a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts +++ b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts @@ -33,6 +33,7 @@ export class TableDesignerComponentInput implements DesignerComponentInput { private _onEditProcessed = new Emitter(); private _onRefreshRequested = new Emitter(); private _originalViewModel: DesignerViewModel; + private _tableDesignerView: azdata.designers.TableDesignerView; public readonly onInitialized: Event = this._onInitialized.event; public readonly onEditProcessed: Event = this._onEditProcessed.event; @@ -81,6 +82,10 @@ export class TableDesignerComponentInput implements DesignerComponentInput { return this._issues; } + get tableDesignerView(): azdata.designers.TableDesignerView { + return this._tableDesignerView; + } + processEdit(edit: DesignerEdit): void { const telemetryInfo = this.createTelemetryInfo(); telemetryInfo.tableObjectType = this.getObjectTypeFromPath(edit.path); @@ -177,6 +182,14 @@ export class TableDesignerComponentInput implements DesignerComponentInput { } } + async save(): Promise { + if (this.tableDesignerView?.useAdvancedSaveMode) { + await this.openPublishDialog(); + } else { + await this.publishChanges(); + } + } + async openPublishDialog(): Promise { const reportNotificationHandle = this._notificationService.notify({ severity: Severity.Info, @@ -243,24 +256,28 @@ export class TableDesignerComponentInput implements DesignerComponentInput { } } - initialize(): void { + async initialize(): Promise { if (this._view !== undefined || this.pendingAction === 'initialize') { return; } this.updateState(this.valid, this.dirty, 'initialize'); - this._provider.initializeTableDesigner(this.tableInfo).then(result => { + try { + const result = await this._provider.initializeTableDesigner(this.tableInfo); this.doInitialization(result); this._onInitialized.fire(); - }, error => { + } catch (error) { this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.errorInitializingTableDesigner', "An error occurred while initializing the table designer: {0}", error?.message ?? error)); - }); + } } private doInitialization(designerInfo: azdata.designers.TableDesignerInfo): void { + this.tableInfo = designerInfo.tableInfo; this.updateState(true, this.tableInfo.isNewTable); this._viewModel = designerInfo.viewModel; this._originalViewModel = this.tableInfo.isNewTable ? undefined : deepClone(this._viewModel); + this._tableDesignerView = designerInfo.view; + this._issues = designerInfo.issues; this.setDesignerView(designerInfo.view); }