Add support for adding new setting in local.settings.json in add SQL binding quickpick (#17093)

* be able to add new setting in local.settings.json

* cleanup

* addressing comments

* remove todo comment

* addressing comments

* update some strings to uris
This commit is contained in:
Kim Santiago
2021-09-22 14:12:01 -07:00
committed by GitHub
parent 56e9feba1b
commit 4e5c99df02
5 changed files with 134 additions and 34 deletions

View File

@@ -41,12 +41,42 @@ export async function getLocalSettingsJson(localSettingsPath: string): Promise<I
};
}
/**
* Adds a new setting to a project's local.settings.json file
* modified from setLocalAppSetting code from vscode-azurefunctions extension
* @param projectFolder full path to project folder
* @param key Key of the new setting
* @param value Value of the new setting
* @returns true if successful adding the new setting, false if unsuccessful
*/
export async function setLocalAppSetting(projectFolder: string, key: string, value: string): Promise<boolean> {
const localSettingsPath: string = path.join(projectFolder, constants.azureFunctionLocalSettingsFileName);
const settings: ILocalSettingsJson = await getLocalSettingsJson(localSettingsPath);
settings.Values = settings.Values || {};
if (settings.Values[key] === value) {
// don't do anything if it's the same as the existing value
return true;
} else if (settings.Values[key]) {
const result = await vscode.window.showWarningMessage(constants.settingAlreadyExists(key), { modal: true }, constants.yesString);
if (result !== constants.yesString) {
// key already exists and user doesn't want to overwrite it
return false;
}
}
settings.Values[key] = value;
await fse.writeJson(localSettingsPath, settings, { spaces: 2 });
return true;
}
/**
* Gets the Azure Functions project that contains the given file if the project is open in one of the workspace folders
* @param filePath file that the containing project needs to be found for
* @returns filepath of project or undefined if project couldn't be found
* @returns uri of project or undefined if project couldn't be found
*/
export async function getAFProjectContainingFile(filePath: string): Promise<string | undefined> {
export async function getAFProjectContainingFile(fileUri: vscode.Uri): Promise<vscode.Uri | undefined> {
// get functions csprojs in the workspace
const projectPromises = vscode.workspace.workspaceFolders?.map(f => utils.getAllProjectsInFolder(f.uri, '.csproj')) ?? [];
const functionsProjects = (await Promise.all(projectPromises)).reduce((prev, curr) => prev.concat(curr), []).filter(p => isFunctionProject(path.dirname(p.fsPath)));
@@ -56,12 +86,12 @@ export async function getAFProjectContainingFile(filePath: string): Promise<stri
// TODO: figure out which project contains the file
// the new style csproj doesn't list all the files in the project anymore, unless the file isn't in the same folder
// so we can't rely on using that to check
console.error('need to find which project contains the file ' + filePath);
console.error('need to find which project contains the file ' + fileUri.fsPath);
return undefined;
} else if (functionsProjects.length === 0) {
throw new Error(constants.noAzureFunctionsProjectsInWorkspace);
} else {
return functionsProjects[0].fsPath;
return functionsProjects[0];
}
}

View File

@@ -479,5 +479,11 @@ export const connectionStringSettingPlaceholder = localize('connectionStringSett
export const noAzureFunctionsInFile = localize('noAzureFunctionsInFile', "No Azure functions in the current active file");
export const noAzureFunctionsProjectsInWorkspace = localize('noAzureFunctionsProjectsInWorkspace', "No Azure functions projects found in the workspace");
export const addPackage = localize('addPackage', "Add Package");
export const createNewLocalAppSetting = localize('createNewLocalAppSetting', 'Create new local app setting');
export const createNewLocalAppSettingWithIcon = `$(add) ${createNewLocalAppSetting}`;
export const valueMustNotBeEmpty = localize('valueMustNotBeEmpty', "Value must not be empty");
export const enterConnectionStringSettingName = localize('enterConnectionStringSettingName', "Enter connection string setting name");
export const enterConnectionString = localize('enterConnectionString', "Enter connection string");
export function settingAlreadyExists(settingName: string) { return localize('SettingAlreadyExists', 'Local app setting \'{0}\' already exists. Overwrite?', settingName); }
export function failedToParse(errorMessage: string) { return localize('failedToParse', 'Failed to parse "{0}": {1}.', azureFunctionLocalSettingsFileName, errorMessage); }
export function jsonParseError(error: string, line: number, column: number) { return localize('jsonParseError', '{0} near line "{1}", column "{2}"', error, line, column); }

View File

@@ -78,9 +78,9 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined,
}
// 4. ask for connection string setting name
let project: string | undefined;
let projectUri: vscode.Uri | undefined;
try {
project = await azureFunctionsUtils.getAFProjectContainingFile(uri.fsPath);
projectUri = await azureFunctionsUtils.getAFProjectContainingFile(uri);
} catch (e) {
// continue even if there's no AF project found. The binding should still be able to be added as long as there was an azure function found in the file earlier
}
@@ -88,16 +88,80 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined,
let connectionStringSettingName;
// show the settings from project's local.settings.json if there's an AF functions project
// TODO: allow new setting name to get added here and added to local.settings.json
if (project) {
const settings = await azureFunctionsUtils.getLocalSettingsJson(path.join(path.dirname(project!), constants.azureFunctionLocalSettingsFileName));
const existingSettings: string[] = settings.Values ? Object.keys(settings.Values) : [];
if (projectUri) {
let settings;
try {
settings = await azureFunctionsUtils.getLocalSettingsJson(path.join(path.dirname(projectUri.fsPath!), constants.azureFunctionLocalSettingsFileName));
} catch (e) {
void vscode.window.showErrorMessage(e);
return;
}
let existingSettings: (vscode.QuickPickItem & { isCreateNew?: boolean })[] = [];
if (settings?.Values) {
existingSettings = Object.keys(settings.Values).map(setting => {
return {
label: setting
} as vscode.QuickPickItem & { isCreateNew?: boolean };
});
}
existingSettings.unshift({ label: constants.createNewLocalAppSettingWithIcon, isCreateNew: true });
while (!connectionStringSettingName) {
const selectedSetting = await vscode.window.showQuickPick(existingSettings, {
canPickMany: false,
title: constants.selectSetting,
ignoreFocusOut: true
});
if (!selectedSetting) {
// User cancelled
return;
}
if (selectedSetting.isCreateNew) {
const newConnectionStringSettingName = await vscode.window.showInputBox(
{
title: constants.enterConnectionStringSettingName,
ignoreFocusOut: true,
validateInput: input => input ? undefined : constants.nameMustNotBeEmpty
}
) ?? '';
if (!newConnectionStringSettingName) {
// go back to select setting quickpick if user escapes from inputting the setting name in case they changed their mind
continue;
}
const newConnectionStringValue = await vscode.window.showInputBox(
{
title: constants.enterConnectionString,
ignoreFocusOut: true,
validateInput: input => input ? undefined : constants.valueMustNotBeEmpty
}
) ?? '';
if (!newConnectionStringValue) {
// go back to select setting quickpick if user escapes from inputting the value in case they changed their mind
continue;
}
try {
const success = await azureFunctionsUtils.setLocalAppSetting(path.dirname(projectUri.fsPath), newConnectionStringSettingName, newConnectionStringValue);
if (success) {
connectionStringSettingName = newConnectionStringSettingName;
}
} catch (e) {
// display error message and show select setting quickpick again
void vscode.window.showErrorMessage(e);
}
// If user cancels out of this or doesn't want to overwrite an existing setting
// just return them to the select setting quickpick in case they changed their mind
} else {
connectionStringSettingName = selectedSetting.label;
}
}
connectionStringSettingName = await vscode.window.showQuickPick(existingSettings, {
canPickMany: false,
title: constants.selectSetting,
ignoreFocusOut: true
});
} else {
// if no AF project was found or there's more than one AF functions project in the workspace,
// ask for the user to input the setting name
@@ -132,6 +196,6 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined,
}
// 6. Add sql extension package reference to project. If the reference is already there, it doesn't get added again
await packageHelper.addPackageToAFProjectContainingFile(uri.fsPath, constants.sqlExtensionPackageName);
await packageHelper.addPackageToAFProjectContainingFile(uri, constants.sqlExtensionPackageName);
}

View File

@@ -27,27 +27,27 @@ describe('PackageHelper tests', function (): void {
it('Should construct correct add Package Arguments', function (): void {
const packageHelper = new PackageHelper( vscode.window.createOutputChannel('db project test'));
const projectPath = 'dummy\\project\\path.csproj';
const result = packageHelper.constructAddPackageArguments(projectPath, constants.sqlExtensionPackageName);
const projectUri = vscode.Uri.file('dummy\\project\\path.csproj');
const result = packageHelper.constructAddPackageArguments(projectUri, constants.sqlExtensionPackageName);
if (os.platform() === 'win32') {
should(result).equal(` add "dummy\\\\project\\\\path.csproj" package ${constants.sqlExtensionPackageName} --prerelease`);
should(result).equal(` add "\\\\dummy\\\\project\\\\path.csproj" package ${constants.sqlExtensionPackageName} --prerelease`);
}
else {
should(result).equal(` add "dummy/project/path.csproj" package ${constants.sqlExtensionPackageName} --prerelease`);
should(result).equal(` add "/dummy/project/path.csproj" package ${constants.sqlExtensionPackageName} --prerelease`);
}
});
it('Should construct correct add Package Arguments with version', function (): void {
const packageHelper = new PackageHelper( vscode.window.createOutputChannel('db project test'));
const projectPath = 'dummy\\project\\path.csproj';
const result = packageHelper.constructAddPackageArguments(projectPath, constants.sqlExtensionPackageName, constants.VersionNumber);
const projectUri = vscode.Uri.file('dummy\\project\\path.csproj');
const result = packageHelper.constructAddPackageArguments(projectUri, constants.sqlExtensionPackageName, constants.VersionNumber);
if (os.platform() === 'win32') {
should(result).equal(` add "dummy\\\\project\\\\path.csproj" package ${constants.sqlExtensionPackageName} -v ${constants.VersionNumber}`);
should(result).equal(` add "\\\\dummy\\\\project\\\\path.csproj" package ${constants.sqlExtensionPackageName} -v ${constants.VersionNumber}`);
}
else {
should(result).equal(` add "dummy/project/path.csproj" package ${constants.sqlExtensionPackageName} -v ${constants.VersionNumber}`);
should(result).equal(` add "/dummy/project/path.csproj" package ${constants.sqlExtensionPackageName} -v ${constants.VersionNumber}`);
}
});
@@ -55,7 +55,7 @@ describe('PackageHelper tests', function (): void {
sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(undefined);
const spy = sinon.spy(vscode.window, 'showInformationMessage');
await packageHelper.addPackageToAFProjectContainingFile('', constants.sqlExtensionPackageName);
await packageHelper.addPackageToAFProjectContainingFile(vscode.Uri.file(''), constants.sqlExtensionPackageName);
should(spy.calledOnce).be.true('showInformationMessage should have been called exactly once');
});
});

View File

@@ -18,13 +18,13 @@ export class PackageHelper {
/**
* Constructs the parameters for a dotnet add package
* @param projectPath full path to project to add package to
* @param projectUri uri of project to add package to
* @param packageName name of package
* @param packageVersion optional version of package. If none, latest will be pulled in
* @returns string constructed with the arguments for dotnet add package
*/
public constructAddPackageArguments(projectPath: string, packageName: string, packageVersion?: string): string {
projectPath = utils.getQuotedPath(projectPath);
public constructAddPackageArguments(projectUri: vscode.Uri, packageName: string, packageVersion?: string): string {
const projectPath = utils.getQuotedPath(projectUri.fsPath);
if (packageVersion) {
return ` add ${projectPath} package ${packageName} -v ${packageVersion}`;
} else {
@@ -36,14 +36,14 @@ export class PackageHelper {
/**
* Runs dotnet add package to add a package reference to the specified project. If the project already has a package reference
* for this package version, the project file won't get updated
* @param projectPath full path to project to add package to
* @param projectPath uri of project to add package to
* @param packageName name of package
* @param packageVersion optional version of package. If none, latest will be pulled in
*/
public async addPackage(project: string, packageName: string, packageVersion?: string): Promise<void> {
public async addPackage(projectUri: vscode.Uri, packageName: string, packageVersion?: string): Promise<void> {
const addOptions: ShellCommandOptions = {
commandTitle: constants.addPackage,
argument: this.constructAddPackageArguments(project, packageName, packageVersion)
argument: this.constructAddPackageArguments(projectUri, packageName, packageVersion)
};
await this.netCoreTool.runDotnetCommand(addOptions);
@@ -51,13 +51,13 @@ export class PackageHelper {
/**
* Adds specified package to Azure Functions project the specified file is a part of
* @param filePath full path to file to find the containing AF project of to add package reference to
* @param filePath uri of file to find the containing AF project of to add package reference to
* @param packageName package to add reference to
* @param packageVersion optional version of package. If none, latest will be pulled in
*/
public async addPackageToAFProjectContainingFile(filePath: string, packageName: string, packageVersion?: string): Promise<void> {
public async addPackageToAFProjectContainingFile(fileUri: vscode.Uri, packageName: string, packageVersion?: string): Promise<void> {
try {
const project = await azureFunctionsUtils.getAFProjectContainingFile(filePath);
const project = await azureFunctionsUtils.getAFProjectContainingFile(fileUri);
// if no AF projects were found, an error gets thrown from getAFProjectContainingFile(). This check is temporary until
// multiple AF projects in the workspace is handled. That scenario returns undefined and shows an info message telling the