/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type * as azdataType from 'azdata'; import * as vscode from 'vscode'; import * as path from 'path'; import { Deferred, DialogBase } from './dialogBase'; import { IWorkspaceService } from '../common/interfaces'; import * as constants from '../common/constants'; import { IProjectType } from 'dataworkspace'; import { directoryExist } from '../common/utils'; import { IconPathHelper } from '../common/iconHelper'; import { defaultProjectSaveLocation } from '../common/projectLocationHelper'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { WorkspaceService } from '../services/workspaceService'; import { isValidBasename, isValidBasenameErrorMessage } from '../common/pathUtilsHelper'; class NewProjectDialogModel { projectTypeId: string = ''; projectFileExtension: string = ''; name: string = ''; location: string = ''; targetPlatform?: string; sdkStyleProject?: boolean; } export async function openSpecificProjectNewProjectDialog(projectType: IProjectType, workspaceService: WorkspaceService): Promise { const dialog = new NewProjectDialog(workspaceService, projectType); await dialog.open(); await dialog.newDialogPromise; return dialog.projectUri; } export class NewProjectDialog extends DialogBase { public model: NewProjectDialogModel = new NewProjectDialogModel(); public formBuilder: azdataType.FormBuilder | undefined; public targetPlatformDropdownFormComponent: azdataType.FormComponent | undefined; public sdkProjectCheckboxFormComponent: azdataType.FormComponent | undefined; public newProjectDialogComplete: Deferred | undefined; public newDialogPromise: Promise = new Promise((resolve, reject) => this.newProjectDialogComplete = { resolve, reject }); public projectUri: vscode.Uri | undefined; constructor(private workspaceService: IWorkspaceService, private specificProjectType?: IProjectType) { super(constants.NewProjectDialogTitle, 'NewProject', constants.CreateButtonText); // dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogLaunched) .withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }) .send(); } async validate(): Promise { try { // the selected location should be an existing directory const parentDirectoryExists = await directoryExist(this.model.location); if (!parentDirectoryExists) { this.showErrorMessage(constants.ProjectParentDirectoryNotExistError(this.model.location)); return false; } // there shouldn't be an existing sub directory with the same name as the project in the selected location const projectDirectoryExists = await directoryExist(path.join(this.model.location, this.model.name)); if (projectDirectoryExists) { this.showErrorMessage(constants.ProjectDirectoryAlreadyExistError(this.model.name, this.model.location)); return false; } if (await this.workspaceService.validateWorkspace() === false) { return false; } return true; } catch (err) { this.showErrorMessage(err?.message ? err.message : err); return false; } } override onCancelButtonClicked(): void { this.newProjectDialogComplete?.resolve(); this.dispose(); } override async onComplete(): Promise { try { TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogCompleted) .withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId }) .send(); this.projectUri = await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId, this.model.targetPlatform, this.model.sdkStyleProject); this.newProjectDialogComplete?.resolve(); } catch (err) { TelemetryReporter.createErrorEvent2(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogCompleted, err) .withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, error: err?.message ? err.message : err }) .send(); void vscode.window.showErrorMessage(err?.message ? err.message : err); } } protected async initialize(view: azdataType.ModelView): Promise { let allProjectTypes = await this.workspaceService.getAllProjectTypes(); // if a specific project type is specified, only show that one if (this.specificProjectType && allProjectTypes.find(p => p.id === this.specificProjectType?.id)) { allProjectTypes = [this.specificProjectType]; } const projectTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProps({ cards: allProjectTypes.map((projectType: IProjectType) => { return { id: projectType.id, label: projectType.displayName, icon: projectType.icon, descriptions: [ { textValue: projectType.displayName, textStyles: { 'font-size': '13px', 'font-weight': 'bold' } }, { textValue: projectType.description } ] }; }), iconHeight: '75px', iconWidth: '75px', cardWidth: '215px', cardHeight: '195px', ariaLabel: constants.TypeTitle, width: '500px', iconPosition: 'top', selectedCardId: allProjectTypes.length > 0 ? allProjectTypes[0].id : undefined }).component(); this.register(projectTypeRadioCardGroup.onSelectionChanged(async (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?.insertFormItem(this.targetPlatformDropdownFormComponent!, 3); } 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; } if (selectedProject?.sdkStyleOption) { sdkProjectCheckbox.checked = true; this.model.sdkStyleProject = true; if (selectedProject.sdkStyleLearnMoreUrl) { await sdkLearnMore.updateProperty('url', selectedProject.sdkStyleLearnMoreUrl); sdkFormComponentGroup.addItem(sdkLearnMore); } else { // remove learn more link if the project type didn't provide it sdkFormComponentGroup.removeItem(sdkLearnMore); } this.formBuilder?.addFormItem(this.sdkProjectCheckboxFormComponent!); } else { this.model.sdkStyleProject = false; this.formBuilder?.removeFormItem(this.sdkProjectCheckboxFormComponent!); } })); const projectNameTextBox = view.modelBuilder.inputBox().withValidation( component => isValidBasename(component.value) ) .withProps({ ariaLabel: constants.ProjectNameTitle, placeHolder: constants.ProjectNamePlaceholder, required: true, width: constants.DefaultInputWidth }).component(); this.register(projectNameTextBox.onTextChanged(text => { const errorMessage = isValidBasenameErrorMessage(text); if (errorMessage !== undefined) { // Set validation error message if project name is invalid return void projectNameTextBox.updateProperty('validationErrorMessage', errorMessage); } else { this.model.name = projectNameTextBox.value!; return projectNameTextBox.updateProperty('title', projectNameTextBox.value); } })); const locationTextBox = view.modelBuilder.inputBox().withProps({ ariaLabel: constants.ProjectLocationTitle, placeHolder: constants.ProjectLocationPlaceholder, required: true, width: constants.DefaultInputWidth }).component(); // use the default save location if it's there // otherwise, if there's an open folder, set the initial location to that. If there are multiple folders in the workspace, default to the first one const defaultSaveLocation = defaultProjectSaveLocation(); locationTextBox.value = defaultSaveLocation?.fsPath ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; this.register(locationTextBox.onTextChanged(() => { this.model.location = locationTextBox.value!; return locationTextBox.updateProperty('title', locationTextBox.value); })); const browseFolderButton = view.modelBuilder.button().withProps({ ariaLabel: constants.BrowseButtonText, title: constants.BrowseButtonText, iconPath: IconPathHelper.folder, height: '16px', width: '18px' }).component(); this.register(browseFolderButton.onDidClick(async () => { let folderUris = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: locationTextBox.value ? vscode.Uri.file(locationTextBox.value) : undefined }); if (!folderUris || folderUris.length === 0) { return; } const selectedFolder = folderUris[0].fsPath; locationTextBox.value = selectedFolder; this.model.location = selectedFolder; })); 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 }; const sdkProjectCheckbox = view.modelBuilder.checkBox().withProps({ checked: true, label: constants.SdkStyleProject }).component(); this.register(sdkProjectCheckbox.onChanged(() => { this.model.sdkStyleProject = sdkProjectCheckbox.checked; })); const sdkLearnMore = view.modelBuilder.hyperlink().withProps({ label: constants.LearnMore, url: '' }).component(); const sdkFormComponentGroup = view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'row', alignItems: 'baseline' }) .withItems([sdkProjectCheckbox], { CSSStyles: { flex: '0 0 auto', 'margin-right': '10px' } }) .component(); if (allProjectTypes[0].sdkStyleLearnMoreUrl) { await sdkLearnMore.updateProperty('url', allProjectTypes[0].sdkStyleLearnMoreUrl); sdkFormComponentGroup.addItem(sdkLearnMore); } this.sdkProjectCheckboxFormComponent = { component: sdkFormComponentGroup, }; this.formBuilder = view.modelBuilder.formContainer().withFormItems([ { title: constants.TypeTitle, component: projectTypeRadioCardGroup }, { title: constants.ProjectNameTitle, required: true, component: this.createHorizontalContainer(view, [projectNameTextBox]) }, { title: constants.ProjectLocationTitle, required: true, component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton]) } ]); // add version dropdown if the first project type has one if (allProjectTypes[0].targetPlatforms) { this.formBuilder.addFormItem(this.targetPlatformDropdownFormComponent); } // add sdk style checkbox is the first project has the option if (allProjectTypes[0].sdkStyleOption) { this.formBuilder.addFormItem(this.sdkProjectCheckboxFormComponent); } 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; } } }