/*--------------------------------------------------------------------------------------------- * 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 { DialogBase } from './dialogBase'; import * as constants from '../common/constants'; import { IWorkspaceService } from '../common/interfaces'; import { directoryExist, fileExist } from '../common/utils'; import { IconPathHelper } from '../common/iconHelper'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { defaultProjectSaveLocation } from '../common/projectLocationHelper'; export class OpenExistingDialog extends DialogBase { public filePathTextBox: azdataType.InputBoxComponent | undefined; public filePathAndButtonComponent: azdataType.FormComponent | undefined; public gitRepoTextBoxComponent: azdataType.FormComponent | undefined; public localClonePathComponent: azdataType.FormComponent | undefined; public localClonePathTextBox: azdataType.InputBoxComponent | undefined; public localRadioButton: azdataType.RadioButtonComponent | undefined; public remoteGitRepoRadioButton: azdataType.RadioButtonComponent | undefined; public locationRadioButtonFormComponent: azdataType.FormComponent | undefined; public formBuilder: azdataType.FormBuilder | undefined; constructor(private workspaceService: IWorkspaceService) { super(constants.OpenExistingDialogTitle, 'OpenProject', constants.OpenButtonText); // dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpenExistingDialogLaunched) .withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }) .send(); } async validate(): Promise { try { if (this.localRadioButton?.checked) { await this.validateFile(this.filePathTextBox!.value!, constants.Project.toLowerCase()); } else { await this.validateClonePath(this.localClonePathTextBox!.value); } if (await this.workspaceService.validateWorkspace() === false) { return false; } return true; } catch (err) { this.showErrorMessage(err?.message ? err.message : err); return false; } } public async validateFile(file: string, fileType: string): Promise { const fileExists = await fileExist(file); if (!fileExists) { throw new Error(constants.FileNotExistError(fileType, file)); } } public async validateClonePath(location: string): Promise { // only need to check if parent directory exists // if the same repo has been cloned before, the git clone will append the next number to the folder const parentDirectoryExists = await directoryExist(location); if (!parentDirectoryExists) { throw new Error(constants.CloneParentDirectoryNotExistError(location)); } } override async onComplete(): Promise { try { // save datapoint now because it'll get set to new value during validateWorkspace() const telemetryProps: any = { hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }; let addProjectsPromise: Promise; if (this.remoteGitRepoRadioButton!.checked) { TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.GitClone) .withAdditionalProperties({ selectedTarget: 'project' }) .send(); addProjectsPromise = this.workspaceService.gitCloneProject((this.gitRepoTextBoxComponent?.component).value!, this.localClonePathTextBox!.value!); } else { telemetryProps.cancelled = 'false'; addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this.filePathTextBox!.value!)]); } TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject) .withAdditionalProperties(telemetryProps) .send(); await addProjectsPromise; } catch (err) { void vscode.window.showErrorMessage(err?.message ? err.message : err); } } protected async initialize(view: azdataType.ModelView): Promise { this.localRadioButton = view.modelBuilder.radioButton().withProps({ name: 'location', label: constants.Local, checked: true }).component(); this.register(this.localRadioButton.onDidChangeCheckedState(checked => { if (checked) { this.formBuilder?.removeFormItem(this.gitRepoTextBoxComponent); this.formBuilder?.removeFormItem(this.localClonePathComponent); this.formBuilder?.insertFormItem(this.filePathAndButtonComponent, 1); } })); this.remoteGitRepoRadioButton = view.modelBuilder.radioButton().withProps({ name: 'location', label: constants.RemoteGitRepo }).component(); this.locationRadioButtonFormComponent = { title: constants.LocationSelectorTitle, component: view.modelBuilder.flexContainer() .withItems([this.localRadioButton, this.remoteGitRepoRadioButton], { flex: '0 0 auto', CSSStyles: { 'margin-right': '15px' } }) .withProps({ ariaRole: 'radiogroup' }) .component() }; this.register(this.remoteGitRepoRadioButton.onDidChangeCheckedState(checked => { if (checked) { this.formBuilder?.removeFormItem(this.filePathAndButtonComponent); this.formBuilder?.insertFormItem(this.gitRepoTextBoxComponent, 1); this.formBuilder?.insertFormItem(this.localClonePathComponent, 2); } })); const gitRepoTextBox = view.modelBuilder.inputBox().withProps({ ariaLabel: constants.GitRepoUrlTitle, placeHolder: constants.GitRepoUrlPlaceholder, required: true, width: constants.DefaultInputWidth }).component(); this.register(gitRepoTextBox.onTextChanged(() => { return gitRepoTextBox.updateProperty('title', this.localClonePathTextBox!.value!); })); this.gitRepoTextBoxComponent = { title: constants.GitRepoUrlTitle, component: gitRepoTextBox }; this.localClonePathTextBox = view.modelBuilder.inputBox().withProps({ ariaLabel: constants.LocalClonePathTitle, placeHolder: constants.LocalClonePathPlaceholder, required: true, width: constants.DefaultInputWidth }).component(); this.register(this.localClonePathTextBox.onTextChanged(() => { return this.localClonePathTextBox!.updateProperty('title', this.localClonePathTextBox!.value!); })); const localClonePathBrowseFolderButton = view.modelBuilder.button().withProps({ ariaLabel: constants.BrowseButtonText, title: constants.BrowseButtonText, iconPath: IconPathHelper.folder, width: '18px', height: '16px', }).component(); this.register(localClonePathBrowseFolderButton.onDidClick(async () => { const folderUris = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: defaultProjectSaveLocation() }); if (!folderUris || folderUris.length === 0) { return; } const selectedFolder = folderUris[0].fsPath; this.localClonePathTextBox!.value = selectedFolder; void this.localClonePathTextBox!.updateProperty('title', this.localClonePathTextBox!.value); })); this.localClonePathComponent = { title: constants.LocalClonePathTitle, component: this.createHorizontalContainer(view, [this.localClonePathTextBox, localClonePathBrowseFolderButton]), required: true }; this.filePathTextBox = view.modelBuilder.inputBox().withProps({ ariaLabel: constants.ProjectFileTitle, placeHolder: constants.ProjectFilePlaceholder, required: true, width: constants.DefaultInputWidth }).component(); this.register(this.filePathTextBox.onTextChanged(() => { return this.filePathTextBox!.updateProperty('title', this.filePathTextBox!.value!); })); const localProjectBrowseFolderButton = view.modelBuilder.button().withProps({ ariaLabel: constants.BrowseButtonText, title: constants.BrowseButtonText, iconPath: IconPathHelper.folder, width: '18px', height: '16px' }).component(); this.register(localProjectBrowseFolderButton.onDidClick(() => this.onBrowseButtonClick())); const flexContainer = this.createHorizontalContainer(view, [this.filePathTextBox, localProjectBrowseFolderButton]); this.filePathAndButtonComponent = { component: flexContainer, title: constants.ProjectFileTitle, required: true }; this.formBuilder = view.modelBuilder.formContainer().withFormItems([ this.locationRadioButtonFormComponent, this.filePathAndButtonComponent, ]); await view.initializeModel(this.formBuilder?.component()); this.initDialogComplete?.resolve(); } public async onBrowseButtonClick(): Promise { const projectFilePath = await browseForProject(this.workspaceService); if (projectFilePath) { this.filePathTextBox!.value = projectFilePath.fsPath; } } } export async function browseForProject(workspaceService: IWorkspaceService): Promise { const filters: { [name: string]: string[] } = {}; const projectTypes = await workspaceService.getAllProjectTypes(); filters[constants.AllProjectTypes] = [...new Set(projectTypes.map(type => type.projectFileExtension))]; projectTypes.forEach(type => { filters[type.displayName] = [type.projectFileExtension]; }); const fileUris = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, openLabel: constants.SelectProjectFileActionName, filters: filters, defaultUri: defaultProjectSaveLocation() }); return fileUris?.[0]; }