diff --git a/extensions/data-workspace/src/common/constants.ts b/extensions/data-workspace/src/common/constants.ts index 1c7fb35f63..bc0f97b0ad 100644 --- a/extensions/data-workspace/src/common/constants.ts +++ b/extensions/data-workspace/src/common/constants.ts @@ -46,6 +46,8 @@ export const ProjectDirectoryAlreadyExistErrorShort = (projectName: string) => { export const SelectProjectType = localize('dataworkspace.selectProjectType', "Select Project Type"); export const SelectProjectLocation = localize('dataworkspace.selectProjectLocation', "Select Project Location"); export const NameCannotBeEmpty = localize('dataworkspace.nameCannotBeEmpty', "Name cannot be empty"); +export const TargetPlatform = localize('dataworkspace.targetPlatform', "Target Platform"); + //Open Existing Dialog export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open Existing Project"); export const FileNotExistError = (fileType: string, filePath: string): string => { return localize('dataworkspace.fileNotExistError', "The selected {0} file '{1}' does not exist or is not a file.", fileType, filePath); }; diff --git a/extensions/data-workspace/src/common/interfaces.ts b/extensions/data-workspace/src/common/interfaces.ts index 1fa696ee52..98b6e7fbb1 100644 --- a/extensions/data-workspace/src/common/interfaces.ts +++ b/extensions/data-workspace/src/common/interfaces.ts @@ -71,8 +71,9 @@ export interface IWorkspaceService { * @param name The name of the project * @param location The location of the project * @param projectTypeId The project type id + * @param projectTargetPlatform The target platform of the project */ - createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise; + createProject(name: string, location: vscode.Uri, projectTypeId: string, projectTargetPlatform?: string): Promise; /** * Clones git repository and adds projects to workspace diff --git a/extensions/data-workspace/src/dataworkspace.d.ts b/extensions/data-workspace/src/dataworkspace.d.ts index 885074c88d..fa169736a3 100644 --- a/extensions/data-workspace/src/dataworkspace.d.ts +++ b/extensions/data-workspace/src/dataworkspace.d.ts @@ -57,8 +57,9 @@ declare module 'dataworkspace' { * @param name Create a project * @param location the parent directory of the project * @param projectTypeId the identifier of the selected project type + * @param projectTargetPlatform the target platform of the project */ - createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise; + createProject(name: string, location: vscode.Uri, projectTypeId: string, projectTargetPlatform?: string): Promise; /** * Gets the project data corresponding to the project file, to be placed in the dashboard container @@ -108,7 +109,17 @@ declare module 'dataworkspace' { /** * Gets the icon path of the project type */ - readonly icon: azdata.IconPath + readonly icon: azdata.IconPath; + + /** + * Gets the target platforms that can be selected when creating a new project + */ + readonly targetPlatforms?: string[]; + + /** + * Gets the default target platform + */ + readonly defaultTargetPlatform?: string; } /** diff --git a/extensions/data-workspace/src/dialogs/newProjectDialog.ts b/extensions/data-workspace/src/dialogs/newProjectDialog.ts index 19c86f8eb5..7284fc60c4 100644 --- a/extensions/data-workspace/src/dialogs/newProjectDialog.ts +++ b/extensions/data-workspace/src/dialogs/newProjectDialog.ts @@ -20,9 +20,13 @@ class NewProjectDialogModel { projectFileExtension: string = ''; name: string = ''; location: string = ''; + targetPlatform?: string; } + export class NewProjectDialog extends DialogBase { public model: NewProjectDialogModel = new NewProjectDialogModel(); + public formBuilder: azdataType.FormBuilder | undefined; + public targetPlatformDropdownFormComponent: azdataType.FormComponent | undefined; constructor(private workspaceService: IWorkspaceService) { super(constants.NewProjectDialogTitle, 'NewProject', constants.CreateButtonText); @@ -67,7 +71,7 @@ export class NewProjectDialog extends DialogBase { .withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId }) .send(); - await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId); + await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId, this.model.targetPlatform); } catch (err) { @@ -81,7 +85,7 @@ export class NewProjectDialog extends DialogBase { protected async initialize(view: azdataType.ModelView): Promise { const allProjectTypes = await this.workspaceService.getAllProjectTypes(); - const projectTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties({ + const projectTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProps({ cards: allProjectTypes.map((projectType: IProjectType) => { return { id: projectType.id, @@ -112,9 +116,22 @@ export class NewProjectDialog extends DialogBase { this.register(projectTypeRadioCardGroup.onSelectionChanged((e) => { this.model.projectTypeId = e.cardId; + const selectedProject = allProjectTypes.find(p => p.id === e.cardId); + + if (selectedProject?.targetPlatforms) { + // update the target platforms dropdown for the selected project type + targetPlatformDropdown.values = selectedProject?.targetPlatforms; + targetPlatformDropdown.value = this.getDefaultTargetPlatform(selectedProject); + + this.formBuilder?.addFormItem(this.targetPlatformDropdownFormComponent!); + } else { + // remove the target version dropdown if the selected project type didn't provide values for this + this.formBuilder?.removeFormItem(this.targetPlatformDropdownFormComponent!); + this.model.targetPlatform = undefined; + } })); - const projectNameTextBox = view.modelBuilder.inputBox().withProperties({ + const projectNameTextBox = view.modelBuilder.inputBox().withProps({ ariaLabel: constants.ProjectNameTitle, placeHolder: constants.ProjectNamePlaceholder, required: true, @@ -126,7 +143,7 @@ export class NewProjectDialog extends DialogBase { projectNameTextBox.updateProperty('title', projectNameTextBox.value); })); - const locationTextBox = view.modelBuilder.inputBox().withProperties({ + const locationTextBox = view.modelBuilder.inputBox().withProps({ ariaLabel: constants.ProjectLocationTitle, placeHolder: constants.ProjectLocationPlaceholder, required: true, @@ -138,7 +155,7 @@ export class NewProjectDialog extends DialogBase { locationTextBox.updateProperty('title', locationTextBox.value); })); - const browseFolderButton = view.modelBuilder.button().withProperties({ + const browseFolderButton = view.modelBuilder.button().withProps({ ariaLabel: constants.BrowseButtonText, iconPath: IconPathHelper.folder, height: '16px', @@ -159,7 +176,26 @@ export class NewProjectDialog extends DialogBase { this.model.location = selectedFolder; })); - const form = view.modelBuilder.formContainer().withFormItems([ + const targetPlatformDropdown = view.modelBuilder.dropDown().withProps({ + values: allProjectTypes[0].targetPlatforms, + value: this.getDefaultTargetPlatform(allProjectTypes[0]), + ariaLabel: constants.TargetPlatform, + required: true, + width: constants.DefaultInputWidth + }).component(); + + this.register(targetPlatformDropdown.onValueChanged(() => { + this.model.targetPlatform = targetPlatformDropdown.value! as string; + })); + + + this.targetPlatformDropdownFormComponent = { + title: constants.TargetPlatform, + required: true, + component: targetPlatformDropdown + }; + + this.formBuilder = view.modelBuilder.formContainer().withFormItems([ { title: constants.TypeTitle, required: true, @@ -169,13 +205,34 @@ export class NewProjectDialog extends DialogBase { title: constants.ProjectNameTitle, required: true, component: this.createHorizontalContainer(view, [projectNameTextBox]) - }, { + }, + { title: constants.ProjectLocationTitle, required: true, component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton]) } - ]).component(); - await view.initializeModel(form); + ]); + + // add version dropdown if the first project type has one + if (allProjectTypes[0].targetPlatforms) { + this.formBuilder.addFormItem(this.targetPlatformDropdownFormComponent); + } + + await view.initializeModel(this.formBuilder.component()); this.initDialogComplete?.resolve(); } + + /** + * Gets the default target platform of the project type if there is one + * @param projectType + * @returns + */ + getDefaultTargetPlatform(projectType: IProjectType): string | undefined { + // only return the specified default target platform if it's also included in the project type's array of target platforms + if (projectType.defaultTargetPlatform && projectType.targetPlatforms?.includes(projectType.defaultTargetPlatform)) { + return projectType.defaultTargetPlatform; + } else { + return undefined; + } + } } diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts index 90987640d2..7f65f64016 100644 --- a/extensions/data-workspace/src/services/workspaceService.ts +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -139,10 +139,10 @@ export class WorkspaceService implements IWorkspaceService { return ProjectProviderRegistry.getProviderByProjectExtension(projectType); } - async createProject(name: string, location: vscode.Uri, projectTypeId: string, workspaceFile?: vscode.Uri): Promise { + async createProject(name: string, location: vscode.Uri, projectTypeId: string, projectTargetVersion?: string): Promise { const provider = ProjectProviderRegistry.getProviderByProjectType(projectTypeId); if (provider) { - const projectFile = await provider.createProject(name, location, projectTypeId); + const projectFile = await provider.createProject(name, location, projectTypeId, projectTargetVersion); await this.addProjectsToWorkspace([projectFile]); this._onDidWorkspaceProjectsChange.fire(); return projectFile; diff --git a/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts b/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts index 76cfe72c42..dfcb24a84d 100644 --- a/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts +++ b/extensions/data-workspace/src/test/dialogs/newProjectDialog.test.ts @@ -12,6 +12,7 @@ import { promises as fs } from 'fs'; import { NewProjectDialog } from '../../dialogs/newProjectDialog'; import { WorkspaceService } from '../../services/workspaceService'; import { testProjectType } from '../testUtils'; +import { IProjectType } from 'dataworkspace'; suite('New Project Dialog', function (): void { this.afterEach(() => { @@ -39,5 +40,54 @@ suite('New Project Dialog', function (): void { dialog.model.name = `TestProject_${new Date().getTime()}`; should.equal(await dialog.validate(), true, 'Validation should pass because name is unique and parent directory exists'); }); + + test('Should select correct target platform if provided default', async function (): Promise { + const projectTypeWithTargetPlatforms: IProjectType = { + id: 'tp2', + description: '', + projectFileExtension: 'testproj2', + icon: '', + displayName: 'test project 2', + targetPlatforms: ['platform1', 'platform2', 'platform3'], + defaultTargetPlatform: 'platform2' + }; + + const workspaceServiceMock = TypeMoq.Mock.ofType(); + workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([projectTypeWithTargetPlatforms])); + + const dialog = new NewProjectDialog(workspaceServiceMock.object); + await dialog.open(); + should.equal(dialog.model.targetPlatform, 'platform2', 'Target platform should be platform2'); + + }); + + test('Should handle invalid default target platform', async function (): Promise { + const projectTypeWithTargetPlatforms: IProjectType = { + id: 'tp2', + description: '', + projectFileExtension: 'testproj2', + icon: '', + displayName: 'test project 2', + targetPlatforms: ['platform1', 'platform2', 'platform3'], + defaultTargetPlatform: 'invalid' + }; + + const workspaceServiceMock = TypeMoq.Mock.ofType(); + workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([projectTypeWithTargetPlatforms])); + + const dialog = new NewProjectDialog(workspaceServiceMock.object); + await dialog.open(); + should.equal(dialog.model.targetPlatform, 'platform1', 'Target platform should be platform1 (the first value in target platforms)'); + + }); + + test('Should handle no target platforms provided by project type', async function (): Promise { + const workspaceServiceMock = TypeMoq.Mock.ofType(); + workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType])); + + const dialog = new NewProjectDialog(workspaceServiceMock.object); + await dialog.open(); + should.equal(dialog.model.targetPlatform, undefined, 'Target platform should be undefined'); + }); }); diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 16c50f993a..b8f1ea58e0 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -225,6 +225,9 @@ export function notValidVariableName(name: string) { return localize('notValidVa export function cantAddCircularProjectReference(project: string) { return localize('cantAddCircularProjectReference', "A reference to project '{0}' cannot be added. Adding this project as a reference would cause a circular dependency", project); } export function unableToFindSqlCmdVariable(variableName: string) { return localize('unableToFindSqlCmdVariable', "Unable to find SQLCMD variable '{0}'", variableName); } export function unableToFindDatabaseReference(reference: string) { return localize('unableToFindReference', "Unable to find database reference {0}", reference); } +export function invalidGuid(guid: string) { return localize('invalidGuid', "Specified GUID is invalid: {0}", guid); } +export function invalidTargetPlatform(targetPlatform: string, supportedTargetPlatforms: string[]) { return localize('invalidTargetPlatform', "Invalid target platform: {0}. Supported target platforms: {1}", targetPlatform, supportedTargetPlatforms.toString()); } + // Action types export const deleteAction = localize('deleteAction', 'Delete'); @@ -348,6 +351,7 @@ export const sameDatabaseExampleUsage = 'SELECT * FROM [Schema1].[Table1]'; export function differentDbSameServerExampleUsage(db: string) { return `SELECT * FROM [${db}].[Schema1].[Table1]`; } export function differentDbDifferentServerExampleUsage(server: string, db: string) { return `SELECT * FROM [${server}].[${db}].[Schema1].[Table1]`; } +// Target platforms export const targetPlatformToVersion: Map = new Map([ [SqlTargetPlatform.sqlServer2005, '90'], [SqlTargetPlatform.sqlServer2008, '100'], @@ -363,7 +367,8 @@ export const targetPlatformToVersion: Map = new Map = { diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts index 94a5f6ca66..ce603569c4 100644 --- a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -39,7 +39,9 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide projectFileExtension: constants.sqlprojExtension.replace(/\./g, ''), displayName: constants.emptyProjectTypeDisplayName, description: constants.emptyProjectTypeDescription, - icon: IconPathHelper.colorfulSqlProject + icon: IconPathHelper.colorfulSqlProject, + targetPlatforms: Array.from(constants.targetPlatformToVersion.keys()), + defaultTargetPlatform: constants.defaultTargetPlatform }, { id: constants.edgeSqlDatabaseProjectTypeId,