/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import path = require('path'); import * as vscode from 'vscode'; import * as constants from '../common/constants'; import { getSqlProjectsInWorkspace, getSystemDatabase, validateSqlCmdVariableName } from '../common/utils'; import { DbServerValues, populateResultWithVars } from './utils'; import { AddDatabaseReferenceSettings } from '../controllers/projectController'; import { IDacpacReferenceSettings, INugetPackageReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { Project } from '../models/project'; import { getSystemDbOptions, promptDacpacLocation } from './addDatabaseReferenceDialog'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { ProjectType } from 'mssql'; /** * Create flow for adding a database reference using only VS Code-native APIs such as QuickPick */ export async function addDatabaseReferenceQuickpick(project: Project): Promise { const otherProjectsInWorkspace = (await getSqlProjectsInWorkspace()).filter(p => p.fsPath !== project.projectFilePath); // 1. Prompt for reference type // Only show project option if we have at least one other project in the workspace const referenceTypes = otherProjectsInWorkspace.length > 0 ? [constants.projectLabel, constants.systemDatabase, constants.dacpacText] : [constants.systemDatabase, constants.dacpacText]; // only add nupkg database reference option if project is SDK-style if (project.sqlProjStyle === ProjectType.SdkStyle) { referenceTypes.push(constants.nupkgText); } const referenceType = await vscode.window.showQuickPick( referenceTypes, { title: constants.referenceType, ignoreFocusOut: true }); if (!referenceType) { // User cancelled return undefined; } switch (referenceType) { case constants.projectLabel: return addProjectReference(otherProjectsInWorkspace); case constants.systemDatabase: return addSystemDatabaseReference(project); case constants.dacpacText: return addDacpacReference(project); case constants.nupkgText: return addNupkgReference(); default: console.log(`Unknown reference type ${referenceType}`); return undefined; } } async function addProjectReference(otherProjectsInWorkspace: vscode.Uri[]): Promise { // (steps continued from addDatabaseReferenceQuickpick) // 2. Prompt database project const otherProjectQuickpickItems: (vscode.QuickPickItem & { uri: vscode.Uri })[] = otherProjectsInWorkspace.map(p => { return { label: path.parse(p.fsPath).name, uri: p }; }); const selectedProject = await vscode.window.showQuickPick( otherProjectQuickpickItems, { title: constants.databaseProject, ignoreFocusOut: true, }); if (!selectedProject) { return; } // 3. Prompt location const location = await promptLocation(); if (!location) { // User cancelled return; } const referenceSettings: IProjectReferenceSettings = { projectName: selectedProject.label, projectGuid: '', projectRelativePath: undefined, databaseName: undefined, databaseVariable: undefined, serverName: undefined, serverVariable: undefined, suppressMissingDependenciesErrors: false }; const dbServerValues = await promptDbServerValues(location, selectedProject.label); if (!dbServerValues) { // User cancelled return; } populateResultWithVars(referenceSettings, dbServerValues); // 7. Prompt suppress unresolved ref errors const suppressErrors = await promptSuppressUnresolvedRefErrors(); referenceSettings.suppressMissingDependenciesErrors = suppressErrors; TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addDatabaseReference) .withAdditionalProperties({ referenceType: constants.projectLabel }) .send(); return referenceSettings; } async function addSystemDatabaseReference(project: Project): Promise { // (steps continued from addDatabaseReferenceQuickpick) // 2. Prompt System DB const selectedSystemDb = await vscode.window.showQuickPick( getSystemDbOptions(project), { title: constants.systemDatabase, ignoreFocusOut: true, }); if (!selectedSystemDb) { // User cancelled return undefined; } // 3. Prompt DB name const dbName = await promptDbName(selectedSystemDb); // 4. Prompt suppress unresolved ref errors const suppressErrors = await promptSuppressUnresolvedRefErrors(); TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addDatabaseReference) .withAdditionalProperties({ referenceType: constants.systemDatabase }) .send(); return { databaseVariableLiteralValue: dbName, systemDb: getSystemDatabase(selectedSystemDb), suppressMissingDependenciesErrors: suppressErrors }; } async function addDacpacReference(project: Project): Promise { // (steps continued from addDatabaseReferenceQuickpick) // 2. Prompt for location const location = await promptLocation(); if (!location) { // User cancelled return undefined; } // 3. Prompt for dacpac location // Show quick pick with just browse option to give user context about what the file dialog is for (since that doesn't always have a title) let dacPacLocation; while (!dacPacLocation) { const browseSelected = await vscode.window.showQuickPick( [constants.browseEllipsisWithIcon], { title: constants.selectDacpac, ignoreFocusOut: true, placeHolder: constants.dacpacMustBeOnSameDrive }); if (!browseSelected) { return undefined; } dacPacLocation = (await promptDacpacLocation())?.[0]; if (!dacPacLocation) { // User cancelled return undefined; } // only support adding dacpacs that are on the same drive as the sqlproj const projectDrive = path.parse(project.projectFilePath).root; const dacpacDrive = path.parse(dacPacLocation.fsPath).root; if (projectDrive !== dacpacDrive) { void vscode.window.showErrorMessage(constants.dacpacNotOnSameDrive(project.projectFilePath)); // set dacPacLocation to undefined so that the browse quickpick will show again dacPacLocation = undefined; } } // 4. Prompt for db/server values const dbServerValues = await promptDbServerValues(location, path.parse(dacPacLocation.fsPath).name); if (!dbServerValues) { // User cancelled return; } // 5. Prompt suppress unresolved ref errors const suppressErrors = await promptSuppressUnresolvedRefErrors(); // 6. Construct result const referenceSettings: IDacpacReferenceSettings = { dacpacFileLocation: dacPacLocation, suppressMissingDependenciesErrors: suppressErrors }; populateResultWithVars(referenceSettings, dbServerValues); TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addDatabaseReference) .withAdditionalProperties({ referenceType: constants.dacpacText }) .send(); return referenceSettings; } async function addNupkgReference(): Promise { // (steps continued from addDatabaseReferenceQuickpick) // 2. Prompt for location const location = await promptLocation(); if (!location) { // User cancelled return undefined; } // 3. Prompt for NuGet package name const nupkgName = await vscode.window.showInputBox( { title: constants.nupkgText, placeHolder: constants.nupkgNamePlaceholder, validateInput: (value) => { return value ? undefined : constants.nameMustNotBeEmpty; }, ignoreFocusOut: true }); if (!nupkgName) { // User cancelled return undefined; } // 4. Prompt for NuGet package version const nupkgVersion = await vscode.window.showInputBox( { title: constants.version, placeHolder: constants.versionPlaceholder, validateInput: (value) => { return value ? undefined : constants.versionMustNotBeEmpty; }, ignoreFocusOut: true }); if (!nupkgVersion) { // User cancelled return undefined; } // 5. Prompt for db/server values const dbServerValues = await promptDbServerValues(location, path.parse(nupkgName).name); if (!dbServerValues) { // User cancelled return; } // 6. Prompt suppress unresolved ref errors const suppressErrors = await promptSuppressUnresolvedRefErrors(); // 7. Construct result const referenceSettings: INugetPackageReferenceSettings = { packageName: nupkgName, packageVersion: nupkgVersion, suppressMissingDependenciesErrors: suppressErrors }; populateResultWithVars(referenceSettings, dbServerValues); TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addDatabaseReference) .withAdditionalProperties({ referenceType: constants.nupkgText }) .send(); return referenceSettings; } async function promptLocation(): Promise { return vscode.window.showQuickPick( constants.locationDropdownValues, { title: constants.location, ignoreFocusOut: true, }); } async function promptDbName(defaultValue: string): Promise { return vscode.window.showInputBox( { title: constants.databaseName, value: defaultValue, validateInput: (value) => { return value ? undefined : constants.nameMustNotBeEmpty; }, ignoreFocusOut: true }); } async function promptDbVar(defaultValue: string): Promise { return await vscode.window.showInputBox( { title: constants.databaseVariable, value: defaultValue, validateInput: (value: string) => { return validateSqlCmdVariableName(value) ?? ''; }, ignoreFocusOut: true }) ?? ''; } async function promptServerName(): Promise { return vscode.window.showInputBox( { title: constants.serverName, value: constants.otherServer, validateInput: (value) => { return value ? undefined : constants.nameMustNotBeEmpty; }, ignoreFocusOut: true }); } async function promptServerVar(): Promise { return await vscode.window.showInputBox( { title: constants.serverVariable, value: constants.otherSeverVariable, validateInput: (value: string) => { return validateSqlCmdVariableName(value) ?? ''; }, ignoreFocusOut: true }) ?? ''; } async function promptSuppressUnresolvedRefErrors(): Promise { const selectedOption = await vscode.window.showQuickPick( [constants.noStringDefault, constants.yesString], { title: constants.suppressMissingDependenciesErrors, ignoreFocusOut: true, }); return selectedOption === constants.yesString ? true : false; } async function promptDbServerValues(location: string, defaultDbName: string): Promise { const ret: DbServerValues = {}; // Only prompt db values if the location is on a different db/server if (location !== constants.sameDatabase) { // 4. Prompt database name const dbName = await promptDbName(defaultDbName); if (!dbName) { // User cancelled return undefined; } ret.dbName = dbName; // 5. Prompt db var const dbVar = await promptDbVar(dbName); // DB Variable is optional so treat escape as skipping it (not cancel in this case) ret.dbVariable = dbVar; } // Only prompt server values if location is different server if (location === constants.differentDbDifferentServer) { // 5. Prompt server name const serverName = await promptServerName(); if (!serverName) { // User cancelled return undefined; } ret.serverName = serverName; // 6. Prompt server var const serverVar = await promptServerVar(); if (!serverVar) { // User cancelled return undefined; } ret.serverVariable = serverVar; } return ret; }