* Adding user surveys

* Fixing lang to point to mssql

* Adding loc files

* adding temp userSurvey controller

* adding loc files

* adding boilerplate code

* Adding more boilerplate code

* Adding command

* Finishing survey

* Adding loc files

* Adding option to pass in user id

* Fixing cancelling panel

* Fixing some strings

* fixing stuff

* Refactor user survey page to use record for user answers

* Adding privacy statement

* Adding localization

* Fixing stuff and adding privacy statement link

* remove subtitle

* Making a helper function to get standard form

* Adding standard nps question to a bunch of important features

* Fixing nps prompts
This commit is contained in:
Aasim Khan 2024-10-01 09:59:40 -07:00 коммит произвёл GitHub
Родитель 9cab31d1f5
Коммит 0c71cd2946
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
19 изменённых файлов: 937 добавлений и 1 удалений

Просмотреть файл

@ -188,6 +188,7 @@ async function generateReactWebviewsBundle() {
'tableDesigner': 'src/reactviews/pages/TableDesigner/index.tsx',
'objectExplorerFilter': 'src/reactviews/pages/ObjectExplorerFilter/index.tsx',
'queryResult': 'src/reactviews/pages/QueryResult/index.tsx',
'userSurvey': 'src/reactviews/pages/UserSurvey/index.tsx',
},
bundle: true,
outdir: 'out/src/reactviews/assets',

Просмотреть файл

@ -238,6 +238,16 @@
"Expand All": "Expand All",
"Collapse All": "Collapse All",
"Filter for any field...": "Filter for any field...",
"Microsoft would like your feedback": "Microsoft would like your feedback",
"Overall, how satisfied are you with the MSSQL extension?": "Overall, how satisfied are you with the MSSQL extension?",
"Very Satisfied": "Very Satisfied",
"Satisfied": "Satisfied",
"Dissatisfied": "Dissatisfied",
"Very Dissatisfied": "Very Dissatisfied",
"Submit": "Submit",
"Not likely at all": "Not likely at all",
"Extremely likely": "Extremely likely",
"Privacy Statement": "Privacy Statement",
"Object Explorer Filter": "Object Explorer Filter",
"{0} (filtered)": "{0} (filtered)",
"View More": "View More",
@ -676,6 +686,26 @@
"{1} is the subscription id"
]
},
"How likely it is that you would recommend the MSSQL extension to a friend or colleague?": "How likely it is that you would recommend the MSSQL extension to a friend or colleague?",
"What can we do to improve?": "What can we do to improve?",
"Take Survey": "Take Survey",
"Remind Me Later": "Remind Me Later",
"Don't Show Again": "Don't Show Again",
"Do you mind taking a quick feedback survey about the MSSQL Extension for VS Code?": "Do you mind taking a quick feedback survey about the MSSQL Extension for VS Code?",
"MSSQL Feedback": "MSSQL Feedback",
"Microsoft reviews your feedback to improve our products, so don't share any personal data or confidential/proprietary content.": "Microsoft reviews your feedback to improve our products, so don't share any personal data or confidential/proprietary content.",
"Overall, how satisfied are you with {0}?/{0} is the feature name": {
"message": "Overall, how satisfied are you with {0}?",
"comment": [
"{0} is the feature name"
]
},
"How likely it is that you would recommend {0} to a friend or colleague?/{0} is the feature name": {
"message": "How likely it is that you would recommend {0} to a friend or colleague?",
"comment": [
"{0} is the feature name"
]
},
"Azure sign in failed.": "Azure sign in failed.",
"Error loading Azure subscriptions.": "Error loading Azure subscriptions.",
"No subscriptions set in VS Code's Azure account filter.": "No subscriptions set in VS Code's Azure account filter.",

Просмотреть файл

@ -366,6 +366,15 @@
<trans-unit id="++CODE++7bba5bcc44de61b37dcadb5824fac7130483b749da0c5e9eea5e02765e06e37d">
<source xml:lang="en">Displays the unified data type (including length, scale and precision) for the column</source>
</trans-unit>
<trans-unit id="++CODE++7a417a747e385113d9c481ed4937ff3d5b31de744c4db4312464f6f50729b616">
<source xml:lang="en">Dissatisfied</source>
</trans-unit>
<trans-unit id="++CODE++5ce32ed24291b6ee178d1069ca31842e8ead67e82f070a284cec1ab95498476c">
<source xml:lang="en">Do you mind taking a quick feedback survey about the MSSQL Extension for VS Code?</source>
</trans-unit>
<trans-unit id="++CODE++1d60d0cdcb5408b8e39ed364f675c9cad8a2f6174a413324d1e34437bd41269b">
<source xml:lang="en">Don&apos;t Show Again</source>
</trans-unit>
<trans-unit id="++CODE++464c4ffd019e1e9691dcf0537c797353ef2b1c1d4833d3d463e5b74ae4547344">
<source xml:lang="en">Edit</source>
</trans-unit>
@ -464,6 +473,9 @@
<trans-unit id="++CODE++c67415bcff328a59fd399e2a7ca9691e0044192fb7480ae501644339965d046d">
<source xml:lang="en">Expression</source>
</trans-unit>
<trans-unit id="++CODE++0556bfb755c73e8c60da1230c38f26ce0eaf79dc7bd5f5368371d561e5eab52f">
<source xml:lang="en">Extremely likely</source>
</trans-unit>
<trans-unit id="++CODE++031a8f0f659df890dfd53c92e45295b0f14c997185bae46e168831e403b273f7">
<source xml:lang="en">Failed</source>
</trans-unit>
@ -562,6 +574,13 @@
<trans-unit id="++CODE++881cda150f08bbb74d0fa62b161f8a953cf0e98bf734b60d0e059b408b75522b">
<source xml:lang="en">Highlight Expensive Operation</source>
</trans-unit>
<trans-unit id="++CODE++cdcc527989217aea79f51128ad3c86fa74d6c9def496f199f6b8d9728791d3b4">
<source xml:lang="en">How likely it is that you would recommend the MSSQL extension to a friend or colleague?</source>
</trans-unit>
<trans-unit id="++CODE++31a89f9bfc36d6f9ecf0a7e2e82cb5a9a0f1e7b1702f08bd3aced239c4f48a0b">
<source xml:lang="en">How likely it is that you would recommend {0} to a friend or colleague?</source>
<note>{0} is the feature name</note>
</trans-unit>
<trans-unit id="++CODE++9638b09db2ce74e3d600f9a512a6116df9ba9b3a147569930a5d5b4682ac5a20">
<source xml:lang="en">I have read the summary and understand the potential risks.</source>
</trans-unit>
@ -627,6 +646,9 @@
<trans-unit id="++CODE++aaaeb8d7872b0c49ed19d951314516d2dcd84ca23411320f05fac3d87bac8a32">
<source xml:lang="en">MSSQL</source>
</trans-unit>
<trans-unit id="++CODE++9f67971ab8a605f2aedf0b33a5fce37d2b135aef48f886231da5d8163aa488a6">
<source xml:lang="en">MSSQL Feedback</source>
</trans-unit>
<trans-unit id="++CODE++614e5389e87ce465fe548a0665aeaa67a52a06ff792f2f791615cd37fb610178">
<source xml:lang="en">Manage Connection Profiles</source>
</trans-unit>
@ -667,6 +689,12 @@
<source xml:lang="en">Microsoft Entra account {0} successfully added.</source>
<note>{0} is the account name</note>
</trans-unit>
<trans-unit id="++CODE++951f4af90cfde827d87f358b3d8d275ee60a3306d9ddb5c4615483cff067697a">
<source xml:lang="en">Microsoft reviews your feedback to improve our products, so don&apos;t share any personal data or confidential/proprietary content.</source>
</trans-unit>
<trans-unit id="++CODE++7d4b99d7fea1f8fb56040b0f388071d3f886f709000859485129c0ce1930fce9">
<source xml:lang="en">Microsoft would like your feedback</source>
</trans-unit>
<trans-unit id="++CODE++b58330ac25057a441365f4f4f1be20daba2d6d940142c891231e0eac66843ff4">
<source xml:lang="en">Move Down</source>
</trans-unit>
@ -741,6 +769,9 @@
<trans-unit id="++CODE++544330f40460cbf3595ddb51445d81c643d5cd9dc72d601db887c01169b5f388">
<source xml:lang="en">Not Starts With</source>
</trans-unit>
<trans-unit id="++CODE++972711d90594be0ec9340bc56950dc455b2764187dcad7093ae641df2cbc10c5">
<source xml:lang="en">Not likely at all</source>
</trans-unit>
<trans-unit id="++CODE++ba35f0c47d862763dafa955d6716942f79b8bfe1d01d5968520db6f9ba665f6f">
<source xml:lang="en">Not started</source>
</trans-unit>
@ -789,6 +820,13 @@
<trans-unit id="++CODE++da53ba1a285ffae9c6528e235b57336fa85b4f05e9fc00a51ee922069c3d9865">
<source xml:lang="en">Optional (False)</source>
</trans-unit>
<trans-unit id="++CODE++eabaa5ba70b7871bd005170e9a540a993456433cdaad54eacc4e4c07a13c71bb">
<source xml:lang="en">Overall, how satisfied are you with the MSSQL extension?</source>
</trans-unit>
<trans-unit id="++CODE++cd184ad9e92906aa47b1a2a184c7d63d4c3642b0ee74079f2d428841ff17036c">
<source xml:lang="en">Overall, how satisfied are you with {0}?</source>
<note>{0} is the feature name</note>
</trans-unit>
<trans-unit id="++CODE++e68b36b17cbd990802f57741cb75cf3a73fa66a61999b0cd70e3cf7d26cfb25f">
<source xml:lang="en">Parameters</source>
</trans-unit>
@ -826,6 +864,9 @@
<trans-unit id="++CODE++a0de89c19964a6454c6d6b4f4205b8c8fcb6c1bfe9370b6d3183226ce8009141">
<source xml:lang="en">Primary Key Columns</source>
</trans-unit>
<trans-unit id="++CODE++25f4fe8cd149e57de765fa487f6e70395ed29ad8d1f2b9c116f9efa24262b420">
<source xml:lang="en">Privacy Statement</source>
</trans-unit>
<trans-unit id="++CODE++a423b47777783386516c79baa19b0dfcd12cb40e4fba79348ad14ab0169402cd">
<source xml:lang="en">Profile Name</source>
</trans-unit>
@ -883,6 +924,9 @@
<trans-unit id="++CODE++0cd1ba1d31d508fcf599096e647eb0c1a60819928248e404a3f71ea9f7aafad8">
<source xml:lang="en">Reload Visual Studio Code</source>
</trans-unit>
<trans-unit id="++CODE++b69ef66763bd411348c2bf030ab4cd0881dc36c538ce1a05966e92b927d2675c">
<source xml:lang="en">Remind Me Later</source>
</trans-unit>
<trans-unit id="++CODE++c3812fc4acb861d5182fc2b8155f327f736fbe5e5eb86a7bd7afcb6dc5497282">
<source xml:lang="en">Remove</source>
</trans-unit>
@ -917,6 +961,9 @@
<trans-unit id="++CODE++bcb563c464628dce28a629cdda03742239a5b0f9e25da0a87aea6e68ad25933a">
<source xml:lang="en">SQL Login</source>
</trans-unit>
<trans-unit id="++CODE++be3bac2c67dcc1b486b10add2ba1bf56e6e49fd17d6187fe6b6c012b08cedbfc">
<source xml:lang="en">Satisfied</source>
</trans-unit>
<trans-unit id="++CODE++caf4128d5bf679fef30d60d545684f44efcc2e9a7098df16fcbd0b625580c89f">
<source xml:lang="en">Save Password</source>
</trans-unit>
@ -1032,6 +1079,9 @@
<trans-unit id="++CODE++72927b6fdb5388115d478bb5e0e69c203231f35ad2e0721d77750626ea4fe4db">
<source xml:lang="en">Starts With</source>
</trans-unit>
<trans-unit id="++CODE++155f816c0407310c0dab222493370773e045ee7fe04e6c9a951b07f495531264">
<source xml:lang="en">Submit</source>
</trans-unit>
<trans-unit id="++CODE++4999c6c6c7badf456f5b0053b09a2076bdb2391a09ab10c9064a680f8f9a0b30">
<source xml:lang="en">Subscription</source>
</trans-unit>
@ -1059,6 +1109,9 @@
<trans-unit id="++CODE++529667eb9a218f074e24ec63181bb6b3bd4e5ea744e64f71262b6323251ed743">
<source xml:lang="en">Table name</source>
</trans-unit>
<trans-unit id="++CODE++9981cdae853624ee8dffbae9510a8f8b9d588788aab84587374f6dd6bc7eabdd">
<source xml:lang="en">Take Survey</source>
</trans-unit>
<trans-unit id="++CODE++e23969d284c3424c8014c6e5b1b85ebc275bc5c74321e7677a21d023e6ea154c">
<source xml:lang="en">Tenant</source>
</trans-unit>
@ -1195,12 +1248,21 @@
<trans-unit id="++CODE++8e37953d23daca5ff01b8282c33f4e0a2152f1d1885f94c06418617e3ee1d24e">
<source xml:lang="en">Value</source>
</trans-unit>
<trans-unit id="++CODE++7ef8939f723e18dae31afe6a66699cdd093848ce10e84fcb5d34e15950cc2a06">
<source xml:lang="en">Very Dissatisfied</source>
</trans-unit>
<trans-unit id="++CODE++421600005b3f0bef9188978d2e890f5c5ff1cdaafa4da92f07a52cdc6295c1c6">
<source xml:lang="en">Very Satisfied</source>
</trans-unit>
<trans-unit id="++CODE++15435b311c9371437109bd5323292504915507b034803118517fee44e9547a3c">
<source xml:lang="en">View More</source>
</trans-unit>
<trans-unit id="++CODE++ff01d2362e483ddaca44f0e7f02d280bef382c1a3209776e5a11ca21acf2cea4">
<source xml:lang="en">View mssql for Visual Studio Code release notes?</source>
</trans-unit>
<trans-unit id="++CODE++bf3cd82434efadb9ea501ff44c7d52b0dd98b3d11c0097ba88a10b7fd14d9b54">
<source xml:lang="en">What can we do to improve?</source>
</trans-unit>
<trans-unit id="++CODE++4aa3356437232c6d45b29802f09eee6e201660a67cf78e145f39ad1fada6feab">
<source xml:lang="en">Width cannot be 0 or negative</source>
</trans-unit>
@ -1463,6 +1525,9 @@
<trans-unit id="mssql.showGettingStarted">
<source xml:lang="en">Getting Started Guide</source>
</trans-unit>
<trans-unit id="mssql.userFeedback">
<source xml:lang="en">Give Feedback</source>
</trans-unit>
<trans-unit id="mssql.Configuration">
<source xml:lang="en">MSSQL configuration</source>
</trans-unit>

3
media/feedback_dark.svg Normal file
Просмотреть файл

@ -0,0 +1,3 @@
<svg id="b5bfbf48-10dd-4d21-8324-df7de0afa790" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M7.484,10.375a5.6,5.6,0,0,1,1.438.828,5.376,5.376,0,0,1,1.109,1.2,5.682,5.682,0,0,1,.711,1.461A5.5,5.5,0,0,1,11,15.5V16H10v-.5a4.385,4.385,0,0,0-.352-1.75,4.459,4.459,0,0,0-.968-1.43,4.654,4.654,0,0,0-1.43-.961A4.458,4.458,0,0,0,5.5,11a4.385,4.385,0,0,0-1.75.352,4.459,4.459,0,0,0-1.43.968,4.654,4.654,0,0,0-.961,1.43A4.458,4.458,0,0,0,1,15.5V16H0v-.5a5.391,5.391,0,0,1,.969-3.1,5.7,5.7,0,0,1,1.109-1.187,5.614,5.614,0,0,1,1.438-.836,3.359,3.359,0,0,1-.633-.563,3.551,3.551,0,0,1-.477-.687,3.387,3.387,0,0,1-.3-.781A3.858,3.858,0,0,1,2,7.5a3.391,3.391,0,0,1,.273-1.359,3.525,3.525,0,0,1,1.86-1.86A3.5,3.5,0,0,1,5.5,4a3.391,3.391,0,0,1,1.359.273,3.525,3.525,0,0,1,1.86,1.86A3.5,3.5,0,0,1,9,7.5a3.424,3.424,0,0,1-.1.836,3.311,3.311,0,0,1-.3.781,4.035,4.035,0,0,1-.477.695A3.076,3.076,0,0,1,7.484,10.375ZM5.5,10a2.424,2.424,0,0,0,.969-.2,2.523,2.523,0,0,0,.789-.532,2.6,2.6,0,0,0,.539-.8,2.478,2.478,0,0,0,.008-1.946,2.46,2.46,0,0,0-.539-.789,2.746,2.746,0,0,0-.8-.539A2.348,2.348,0,0,0,5.5,5a2.424,2.424,0,0,0-.969.2A2.537,2.537,0,0,0,3.2,6.531a2.505,2.505,0,0,0,0,1.938,2.631,2.631,0,0,0,.532.8,2.436,2.436,0,0,0,.8.539A2.484,2.484,0,0,0,5.5,10ZM16,0V8H14l-3,3V8H10V7h2V8.586L13.586,7H15V1H5V2.8c-.167.021-.333.047-.5.078a2.926,2.926,0,0,0-.5.141V0Z" fill="white" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

3
media/feedback_light.svg Normal file
Просмотреть файл

@ -0,0 +1,3 @@
<svg id="b5bfbf48-10dd-4d21-8324-df7de0afa790" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M7.484,10.375a5.6,5.6,0,0,1,1.438.828,5.376,5.376,0,0,1,1.109,1.2,5.682,5.682,0,0,1,.711,1.461A5.5,5.5,0,0,1,11,15.5V16H10v-.5a4.385,4.385,0,0,0-.352-1.75,4.459,4.459,0,0,0-.968-1.43,4.654,4.654,0,0,0-1.43-.961A4.458,4.458,0,0,0,5.5,11a4.385,4.385,0,0,0-1.75.352,4.459,4.459,0,0,0-1.43.968,4.654,4.654,0,0,0-.961,1.43A4.458,4.458,0,0,0,1,15.5V16H0v-.5a5.391,5.391,0,0,1,.969-3.1,5.7,5.7,0,0,1,1.109-1.187,5.614,5.614,0,0,1,1.438-.836,3.359,3.359,0,0,1-.633-.563,3.551,3.551,0,0,1-.477-.687,3.387,3.387,0,0,1-.3-.781A3.858,3.858,0,0,1,2,7.5a3.391,3.391,0,0,1,.273-1.359,3.525,3.525,0,0,1,1.86-1.86A3.5,3.5,0,0,1,5.5,4a3.391,3.391,0,0,1,1.359.273,3.525,3.525,0,0,1,1.86,1.86A3.5,3.5,0,0,1,9,7.5a3.424,3.424,0,0,1-.1.836,3.311,3.311,0,0,1-.3.781,4.035,4.035,0,0,1-.477.695A3.076,3.076,0,0,1,7.484,10.375ZM5.5,10a2.424,2.424,0,0,0,.969-.2,2.523,2.523,0,0,0,.789-.532,2.6,2.6,0,0,0,.539-.8,2.478,2.478,0,0,0,.008-1.946,2.46,2.46,0,0,0-.539-.789,2.746,2.746,0,0,0-.8-.539A2.348,2.348,0,0,0,5.5,5a2.424,2.424,0,0,0-.969.2A2.537,2.537,0,0,0,3.2,6.531a2.505,2.505,0,0,0,0,1.938,2.631,2.631,0,0,0,.532.8,2.436,2.436,0,0,0,.8.539A2.484,2.484,0,0,0,5.5,10ZM16,0V8H14l-3,3V8H10V7h2V8.586L13.586,7H15V1H5V2.8c-.167.021-.333.047-.5.078a2.926,2.926,0,0,0-.5.141V0Z" fill="black" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Просмотреть файл

@ -794,6 +794,11 @@
"command": "mssql.clearAzureAccountTokenCache",
"title": "%mssql.clearAzureAccountTokenCache%",
"category": "MS SQL"
},
{
"command": "mssql.userFeedback",
"title": "%mssql.userFeedback%",
"category": "MS SQL"
}
],
"keybindings": [

Просмотреть файл

@ -148,5 +148,6 @@
"mssql.filterNode":"Filter",
"mssql.clearFilters":"Clear Filters",
"mssql.openExecutionPlanFile":"Open Execution Plan File",
"mssql.enableExperimentalFeatures.description":"Enables experimental features in the MSSQL extension. The features are not production-ready and may have bugs or issues. Restart Visual Studio Code after changing this setting."
"mssql.enableExperimentalFeatures.description":"Enables experimental features in the MSSQL extension. The features are not production-ready and may have bugs or issues. Restart Visual Studio Code after changing this setting.",
"mssql.userFeedback":"Give Feedback"
}

Просмотреть файл

@ -48,6 +48,7 @@ import {
} from "@azure/arm-resources";
import { getErrorMessage, listAllIterator } from "../utils/utils";
import { l10n } from "vscode";
import { UserSurvey } from "../nps/userSurvey";
export class ConnectionDialogWebviewController extends ReactWebviewPanelController<
ConnectionDialogWebviewState,
@ -933,6 +934,7 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
expand: true,
});
await this.panel.dispose();
await UserSurvey.getInstance().promptUserForNPSFeedback();
} catch (error) {
this.state.connectionStatus = ApiStatus.Error;
return state;

Просмотреть файл

@ -5,6 +5,7 @@
// Collection of Non-localizable Constants
export const languageId = "sql";
export const extensionId = "ms-mssql.mssql";
export const extensionName = "mssql";
export const extensionConfigSectionName = "mssql";
export const telemetryConfigSectionName = "telemetry";
@ -81,6 +82,7 @@ export const cmdClearAzureTokenCache = "mssql.clearAzureAccountTokenCache";
export const cmdNewTable = "mssql.newTable";
export const cmdEditTable = "mssql.editTable";
export const cmdEditConnection = "mssql.editConnection";
export const cmdLaunchUserFeedback = "mssql.userFeedback";
export const piiLogging = "piiLogging";
export const mssqlPiiLogging = "mssql.piiLogging";
export const enableSqlAuthenticationProvider =
@ -210,6 +212,8 @@ export const scriptSelectText = "SELECT TOP (1000) * FROM ";
export const tenantDisplayName = "Microsoft";
export const windowsResourceClientPath = "SqlToolsResourceProviderService.exe";
export const unixResourceClientPath = "SqlToolsResourceProviderService";
export const microsoftPrivacyStatementUrl =
"https://www.microsoft.com/en-us/privacy/privacystatement";
export enum Platform {
Windows = "win32",

Просмотреть файл

@ -677,3 +677,39 @@ export class ConnectionDialog {
});
}
}
export class UserSurvey {
public static overallHowSatisfiedAreYouWithMSSQLExtension = l10n.t(
"Overall, how satisfied are you with the MSSQL extension?",
);
public static howlikelyAreYouToRecommendMSSQLExtension = l10n.t(
"How likely it is that you would recommend the MSSQL extension to a friend or colleague?",
);
public static whatCanWeDoToImprove = l10n.t("What can we do to improve?");
public static takeSurvey = l10n.t("Take Survey");
public static remindMeLater = l10n.t("Remind Me Later");
public static dontShowAgain = l10n.t("Don't Show Again");
public static doYouMindTakingAQuickFeedbackSurvey = l10n.t(
"Do you mind taking a quick feedback survey about the MSSQL Extension for VS Code?",
);
public static mssqlFeedback = l10n.t("MSSQL Feedback");
public static privacyDisclaimer = l10n.t(
"Microsoft reviews your feedback to improve our products, so don't share any personal data or confidential/proprietary content.",
);
public static overallHowStatisfiedAreYouWithFeature = (
featureName: string,
) =>
l10n.t({
message: "Overall, how satisfied are you with {0}?",
args: [featureName],
comment: ["{0} is the feature name"],
});
public static howLikelyAreYouToRecommendFeature = (featureName: string) =>
l10n.t({
message:
"How likely it is that you would recommend {0} to a friend or colleague?",
args: [featureName],
comment: ["{0} is the feature name"],
});
}

Просмотреть файл

@ -57,6 +57,7 @@ import { ExecutionPlanWebviewController } from "./executionPlanWebviewController
import { QueryResultWebviewController } from "../queryResult/queryResultWebViewController";
import { MssqlProtocolHandler } from "../mssqlProtocolHandler";
import { isIConnectionInfo } from "../utils/utils";
import { UserSurvey } from "../nps/userSurvey";
/**
* The main controller class that initializes the extension
@ -109,6 +110,7 @@ export default class MainController implements vscode.Disposable {
this._vscodeWrapper,
);
this.configuration = vscode.workspace.getConfiguration();
UserSurvey.createInstance(this._context);
}
/**
@ -183,6 +185,7 @@ export default class MainController implements vscode.Disposable {
});
this.registerCommand(Constants.cmdRunQuery);
this._event.on(Constants.cmdRunQuery, () => {
UserSurvey.getInstance().promptUserForNPSFeedback();
this.onRunQuery();
});
this.registerCommand(Constants.cmdManageConnectionProfiles);
@ -209,6 +212,10 @@ export default class MainController implements vscode.Disposable {
this._event.on(Constants.cmdChooseLanguageFlavor, () => {
this.runAndLogErrors(this.onChooseLanguageFlavor());
});
this.registerCommand(Constants.cmdLaunchUserFeedback);
this._event.on(Constants.cmdLaunchUserFeedback, async () => {
await UserSurvey.getInstance().promptUserForNPSFeedback();
});
this.registerCommand(Constants.cmdCancelQuery);
this._event.on(Constants.cmdCancelQuery, () => {
this.onCancelQuery();
@ -969,6 +976,7 @@ export default class MainController implements vscode.Disposable {
Constants.cmdScriptSelect,
async (node: TreeNodeInfo) => {
await this.scriptNode(node, ScriptOperation.Select, true);
await UserSurvey.getInstance().promptUserForNPSFeedback();
},
),
);

275
src/nps/userSurvey.ts Normal file
Просмотреть файл

@ -0,0 +1,275 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from "vscode";
import * as constants from "../constants/constants";
import { ReactWebviewPanelController } from "../controllers/reactWebviewController";
import {
UserSurveyReducers,
UserSurveyState,
} from "../sharedInterfaces/userSurvey";
import * as locConstants from "../constants/locConstants";
import { sendActionEvent } from "../telemetry/telemetry";
import {
TelemetryActions,
TelemetryViews,
} from "../sharedInterfaces/telemetry";
const PROBABILITY = 0.15;
const SESSION_COUNT_KEY = "nps/sessionCount";
const LAST_SESSION_DATE_KEY = "nps/lastSessionDate";
const SKIP_VERSION_KEY = "nps/skipVersion";
const IS_CANDIDATE_KEY = "nps/isCandidate";
export class UserSurvey {
private static _instance: UserSurvey;
private _webviewController: UserSurveyWebviewController;
private constructor(private _context: vscode.ExtensionContext) {}
public static createInstance(_context: vscode.ExtensionContext): void {
UserSurvey._instance = new UserSurvey(_context);
}
public static getInstance(): UserSurvey {
return UserSurvey._instance;
}
public async promptUserForNPSFeedback(): Promise<void> {
const globalState = this._context.globalState;
const skipVersion = globalState.get(SKIP_VERSION_KEY, "");
if (skipVersion) {
return;
}
const date = new Date().toDateString();
const lastSessionDate = globalState.get(
LAST_SESSION_DATE_KEY,
new Date(0).toDateString(),
);
if (date === lastSessionDate) {
return;
}
const sessionCount = globalState.get(SESSION_COUNT_KEY, 0) + 1;
await globalState.update(LAST_SESSION_DATE_KEY, date);
await globalState.update(SESSION_COUNT_KEY, sessionCount);
// don't prompt for feedback from users until they've had a chance to use the extension a few times
if (sessionCount < 5) {
return;
}
const isCandidate =
globalState.get(IS_CANDIDATE_KEY, false) ||
Math.random() < PROBABILITY;
await globalState.update(IS_CANDIDATE_KEY, isCandidate);
const extensionVersion =
vscode.extensions.getExtension(constants.extensionId).packageJSON
.version || "unknown";
if (!isCandidate) {
await globalState.update(SKIP_VERSION_KEY, extensionVersion);
return;
}
const take = {
title: locConstants.UserSurvey.takeSurvey,
run: async () => {
const state: UserSurveyState = getStandardNPSQuestions();
if (
!this._webviewController ||
this._webviewController.isDisposed
) {
this._webviewController = new UserSurveyWebviewController(
this._context,
state,
);
} else {
this._webviewController.updateState(state);
}
this._webviewController.revealToForeground();
const answers = await new Promise<Record<string, string>>(
(resolve) => {
this._webviewController.onSubmit((e) => {
resolve(e);
});
this._webviewController.onCancel(() => {
resolve({});
});
},
);
sendActionEvent(
TelemetryViews.UserSurvey,
TelemetryActions.SurverySubmit,
{
surveyId: "nps",
...answers,
},
);
await globalState.update(IS_CANDIDATE_KEY, false);
await globalState.update(SKIP_VERSION_KEY, extensionVersion);
},
};
const remind = {
title: locConstants.UserSurvey.remindMeLater,
run: async () => {
await globalState.update(SESSION_COUNT_KEY, sessionCount - 3);
},
};
const never = {
title: locConstants.UserSurvey.dontShowAgain,
isSecondary: true,
run: async () => {
await globalState.update(IS_CANDIDATE_KEY, false);
await globalState.update(SKIP_VERSION_KEY, extensionVersion);
},
};
const button = await vscode.window.showInformationMessage(
locConstants.UserSurvey.doYouMindTakingAQuickFeedbackSurvey,
take,
remind,
never,
);
await (button || remind).run();
}
public async launchSurvey(
surveyId: string,
survey: UserSurveyState,
): Promise<Record<string, string>> {
const state: UserSurveyState = survey;
if (!this._webviewController || this._webviewController.isDisposed) {
this._webviewController = new UserSurveyWebviewController(
this._context,
state,
);
} else {
this._webviewController.updateState(state);
}
this._webviewController.revealToForeground();
const answers = await new Promise<Record<string, string>>((resolve) => {
this._webviewController.onSubmit((e) => {
resolve(e);
});
this._webviewController.onCancel(() => {
resolve({});
});
});
sendActionEvent(
TelemetryViews.UserSurvey,
TelemetryActions.SurverySubmit,
{
surveyId: surveyId,
...answers,
},
);
return answers;
}
}
class UserSurveyWebviewController extends ReactWebviewPanelController<
UserSurveyState,
UserSurveyReducers
> {
private _onSubmit: vscode.EventEmitter<Record<string, string>> =
new vscode.EventEmitter<Record<string, string>>();
public readonly onSubmit: vscode.Event<Record<string, string>> =
this._onSubmit.event;
private _onCancel: vscode.EventEmitter<void> =
new vscode.EventEmitter<void>();
public readonly onCancel: vscode.Event<void> = this._onCancel.event;
constructor(context: vscode.ExtensionContext, state?: UserSurveyState) {
super(
context,
locConstants.UserSurvey.mssqlFeedback,
"userSurvey",
state,
undefined,
{
dark: vscode.Uri.joinPath(
context.extensionUri,
"media",
"feedback_dark.svg",
),
light: vscode.Uri.joinPath(
context.extensionUri,
"media",
"feedback_light.svg",
),
},
);
this.registerReducer("submit", async (state, payload) => {
this._onSubmit.fire(payload.answers);
this.panel.dispose();
return state;
});
this.registerReducer("cancel", async (state) => {
this._onCancel.fire();
this.panel.dispose();
return state;
});
this.registerReducer("openPrivacyStatement", async (state) => {
vscode.env.openExternal(
vscode.Uri.parse(constants.microsoftPrivacyStatementUrl),
);
return state;
});
this.panel.onDidDispose(() => {
this._onCancel.fire();
});
}
updateState(state: UserSurveyState): void {
this.state = state;
}
}
export function getStandardNPSQuestions(featureName?: string): UserSurveyState {
return {
questions: [
{
label: featureName
? locConstants.UserSurvey.howLikelyAreYouToRecommendFeature(
featureName,
)
: locConstants.UserSurvey
.howlikelyAreYouToRecommendMSSQLExtension,
type: "nps",
required: true,
},
{
label: featureName
? locConstants.UserSurvey.overallHowStatisfiedAreYouWithFeature(
featureName,
)
: locConstants.UserSurvey
.overallHowSatisfiedAreYouWithMSSQLExtension,
type: "nsat",
required: true,
},
{
type: "divider",
},
{
label: locConstants.UserSurvey.whatCanWeDoToImprove,
type: "textarea",
required: false,
placeholder: locConstants.UserSurvey.privacyDisclaimer,
},
],
};
}

Просмотреть файл

@ -205,6 +205,26 @@ export class LocConstants {
filterAnyField: l10n.t("Filter for any field..."),
};
}
public get userFeedback() {
return {
microsoftWouldLikeYourFeedback: l10n.t(
"Microsoft would like your feedback",
),
overallHowSatisfiedAreYouWithMSSQLExtension: l10n.t(
"Overall, how satisfied are you with the MSSQL extension?",
),
verySatisfied: l10n.t("Very Satisfied"),
satisfied: l10n.t("Satisfied"),
dissatisfied: l10n.t("Dissatisfied"),
veryDissatisfied: l10n.t("Very Dissatisfied"),
submit: l10n.t("Submit"),
cancel: l10n.t("Cancel"),
notLikelyAtAll: l10n.t("Not likely at all"),
extremelyLikely: l10n.t("Extremely likely"),
privacyStatement: l10n.t("Privacy Statement"),
};
}
}
export let locConstants = LocConstants.getInstance();

Просмотреть файл

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import ReactDOM from "react-dom/client";
import { VscodeWebviewProvider } from "../../common/vscodeWebviewProvider";
import { UserSurveyStateProvider } from "./userSurveryStateProvider";
import { UserSurveyPage } from "./userSurveyPage";
import "../../index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<VscodeWebviewProvider>
<UserSurveyStateProvider>
<UserSurveyPage />
</UserSurveyStateProvider>
</VscodeWebviewProvider>,
);

Просмотреть файл

@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import React, { createContext } from "react";
import { useVscodeWebview } from "../../common/vscodeWebviewProvider";
import {
UserSurveyContextProps,
UserSurveyState,
UserSurveyReducers,
} from "../../../sharedInterfaces/userSurvey";
const UserSurveyContext = createContext<UserSurveyContextProps | undefined>(
undefined,
);
interface UserSurveyProviderProps {
children: React.ReactNode;
}
const UserSurveyStateProvider: React.FC<UserSurveyProviderProps> = ({
children,
}) => {
const vscodeWebviewProvider = useVscodeWebview<
UserSurveyState,
UserSurveyReducers
>();
return (
<UserSurveyContext.Provider
value={{
state: vscodeWebviewProvider.state,
submit: async (answers: Record<string, string>) => {
await vscodeWebviewProvider.extensionRpc.action("submit", {
answers: answers,
});
},
cancel: async () => {
await vscodeWebviewProvider.extensionRpc.action("cancel");
},
openPrivacyStatement: async () => {
await vscodeWebviewProvider.extensionRpc.action(
"openPrivacyStatement",
);
},
}}
>
{children}
</UserSurveyContext.Provider>
);
};
export { UserSurveyContext, UserSurveyStateProvider };

Просмотреть файл

@ -0,0 +1,326 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Button,
Divider,
Field,
Link,
makeStyles,
Radio,
RadioGroup,
Text,
Textarea,
} from "@fluentui/react-components";
import { useContext, useState } from "react";
import { UserSurveyContext } from "./userSurveryStateProvider";
import { locConstants } from "../../common/locConstants";
import {
BaseQuestion,
NpsQuestion,
NsatQuestion,
TextareaQuestion,
} from "../../../sharedInterfaces/userSurvey";
const useStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
width: "800px",
maxWidth: "100%",
"> *": {
marginBottom: "15px",
},
padding: "10px",
},
title: {
marginBottom: "30px",
},
footer: {
display: "flex",
justifyContent: "space-between",
},
buttonsContainer: {
display: "flex",
"> *": {
marginRight: "10px",
},
},
privacyDisclaimer: {
marginLeft: "auto",
},
});
export const UserSurveyPage = () => {
const classes = useStyles();
const userSurveryProvider = useContext(UserSurveyContext);
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
const [userAnswers, setUserAnswers] = useState<Record<string, string>>({});
const updateSubmitButtonState = () => {
for (let i = 0; i < userSurveryProvider!.state!.questions.length; i++) {
const question = userSurveryProvider!.state!.questions[i];
// if question is not divider and not required, skip
if (question.type === "divider") {
continue;
}
if (!(question as BaseQuestion)?.required) {
continue;
}
if (!userAnswers[question.label]) {
setIsSubmitDisabled(true);
return;
}
}
setIsSubmitDisabled(false);
};
const onAnswerChange = (label: string, answer: string) => {
userAnswers[label] = answer;
setUserAnswers(userAnswers);
updateSubmitButtonState();
};
if (!userSurveryProvider?.state) {
return undefined;
}
return (
<div className={classes.root}>
<h2
style={{
marginBottom: "30px",
}}
>
{userSurveryProvider.state.title ??
locConstants.userFeedback.microsoftWouldLikeYourFeedback}
</h2>
{userSurveryProvider.state.subtitle && (
<p>{userSurveryProvider.state.subtitle}</p>
)}
{userSurveryProvider.state.questions.map((question, index) => {
switch (question.type) {
case "nsat":
return (
<NSATQuestion
key={index}
question={question}
onChange={(d) =>
onAnswerChange(question.label, d)
}
/>
);
case "nps":
return (
<NPSQuestion
key={index}
question={question}
onChange={(d) =>
onAnswerChange(question.label, d)
}
/>
);
case "textarea":
return (
<TextAreaQuestion
key={index}
question={question}
onChange={(d) =>
onAnswerChange(question.label, d)
}
/>
);
case "divider":
return <Divider key={index} />;
default:
return undefined;
}
})}
<div className={classes.footer}>
<div className={classes.buttonsContainer}>
<Button
appearance="primary"
disabled={isSubmitDisabled}
onClick={() => userSurveryProvider.submit(userAnswers)}
>
{userSurveryProvider.state.submitButtonText ??
locConstants.userFeedback.submit}
</Button>
<Button onClick={() => userSurveryProvider.cancel()}>
{userSurveryProvider.state.cancelButtonText ??
locConstants.userFeedback.cancel}
</Button>
</div>
<Link
onClick={() => {
userSurveryProvider.openPrivacyStatement();
}}
>
{locConstants.userFeedback.privacyStatement}
</Link>
</div>
</div>
);
};
export interface QuestionProps<T> {
question: T;
onChange: (data: string) => void;
}
export const NSATQuestion = ({
question,
onChange,
}: QuestionProps<NsatQuestion>) => {
const userSurveryProvider = useContext(UserSurveyContext);
if (!userSurveryProvider) {
return undefined;
}
return (
<Field
label={
<Text weight="bold">
{question.label ??
locConstants.userFeedback
.overallHowSatisfiedAreYouWithMSSQLExtension}
</Text>
}
required={question.required ?? false}
>
<RadioGroup
layout="horizontal-stacked"
onChange={(_e, d) => onChange(d.value)}
>
<Radio
value={locConstants.userFeedback.veryDissatisfied}
label={locConstants.userFeedback.veryDissatisfied}
/>
<Radio
value={locConstants.userFeedback.dissatisfied}
label={locConstants.userFeedback.dissatisfied}
/>
<Radio
value={locConstants.userFeedback.satisfied}
label={locConstants.userFeedback.satisfied}
/>
<Radio
value={locConstants.userFeedback.verySatisfied}
label={locConstants.userFeedback.verySatisfied}
/>
</RadioGroup>
</Field>
);
};
export const NPSQuestion = ({
question,
onChange,
}: QuestionProps<NpsQuestion>) => {
const userSurveryProvider = useContext(UserSurveyContext);
if (!userSurveryProvider) {
return undefined;
}
return (
<Field
label={<Text weight="bold">{question.label}</Text>}
required={question.required ?? false}
style={{
marginBottom: "25px",
}}
>
<RadioGroup
layout="horizontal-stacked"
onChange={(_e, d) => onChange(d.value)}
>
<Radio
value={"0"}
label={
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
}}
>
{"0"}
<br />
<Text
style={{
position: "absolute",
width: "100px",
top: "30px",
left: "0px",
fontSize: "10px",
}}
size={200}
>
{locConstants.userFeedback.notLikelyAtAll}
</Text>
</div>
}
/>
<Radio value={"1"} label={"1"} />
<Radio value={"2"} label={"2"} />
<Radio value={"3"} label={"3"} />
<Radio value={"4"} label={"4"} />
<Radio value={"5"} label={"5"} />
<Radio value={"6"} label={"6"} />
<Radio value={"7"} label={"7"} />
<Radio value={"8"} label={"8"} />
<Radio value={"9"} label={"9"} />
<Radio
value={"10"}
label={
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
}}
>
{"10"}
<br />
<Text
style={{
position: "absolute",
width: "max-content",
top: "30px",
right: "0px",
fontSize: "10px",
}}
size={200}
>
{locConstants.userFeedback.extremelyLikely}
</Text>
</div>
}
/>
</RadioGroup>
</Field>
);
};
export const TextAreaQuestion = ({
question,
onChange,
}: QuestionProps<TextareaQuestion>) => {
const userSurveryProvider = useContext(UserSurveyContext);
if (!userSurveryProvider) {
return undefined;
}
return (
<Field
required={question.required ?? false}
label={<Text weight="bold">{question.label}</Text>}
hint={question.placeholder}
>
<Textarea
onChange={(_e, data) => onChange(data.value)}
resize="vertical"
/>
</Field>
);
};

Просмотреть файл

@ -13,6 +13,7 @@ export enum TelemetryViews {
WebviewController = "WebviewController",
ObjectExplorerFilter = "ObjectExplorerFilter",
TableDesigner = "TableDesigner",
UserSurvey = "UserSurvey",
}
export enum TelemetryActions {
@ -39,6 +40,7 @@ export enum TelemetryActions {
Publish = "Publish",
ContinueEditing = "ContinueEditing",
Close = "Close",
SurverySubmit = "SurveySubmit",
}
export interface WebviewTelemetryActionEvent {

Просмотреть файл

@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface UserSurveyState {
/**
* The title of the survey. By default, it is "Microsoft would like your feedback".
*/
title?: string;
/**
* The subtitle of the survey. By default, it is empty.
*/
subtitle?: string;
/**
* The text of the submit button. By default, it is "Submit".
*/
submitButtonText?: string;
/**
* The text of the cancel button. By default, it is "Cancel".
*/
cancelButtonText?: string;
/**
* The questions of the survey.
*/
questions: Question[];
}
export type Question = NpsQuestion | NsatQuestion | TextareaQuestion | Divider;
export interface BaseQuestion {
/**
* The label of the question.
*/
label: string;
/**
* The required field of the question.
*/
required?: boolean;
}
/**
* A question with a radio button with 0 to 10 options.
*/
export interface NpsQuestion extends BaseQuestion {
type: "nps";
}
/**
* A question with a radio button with 'Very Satisfied', 'Satisfied', 'Dissatisfied', 'Very Dissatisfied' options.
*/
export interface NsatQuestion extends BaseQuestion {
type: "nsat";
}
export interface TextareaQuestion extends BaseQuestion {
type: "textarea";
/**
* The placeholder for the textarea.
*/
placeholder?: string;
}
export interface Divider {
type: "divider";
}
export interface UserSurveyContextProps {
state: UserSurveyState;
submit(answers: Record<string, string>): void;
cancel(): void;
openPrivacyStatement(): void;
}
export interface UserSurveyReducers {
submit: {
answers: Record<string, string>;
};
cancel: {};
openPrivacyStatement: {};
}

Просмотреть файл

@ -17,6 +17,7 @@ import {
TelemetryViews,
} from "../sharedInterfaces/telemetry";
import { scriptCopiedToClipboard } from "../constants/locConstants";
import { UserSurvey } from "../nps/userSurvey";
export class TableDesignerWebviewController extends ReactWebviewPanelController<
designer.TableDesignerWebviewState,
@ -258,6 +259,7 @@ export class TableDesignerWebviewController extends ReactWebviewPanelController<
},
};
this.panel.title = state.tableInfo.title;
await UserSurvey.getInstance().promptUserForNPSFeedback();
return state;
});
@ -287,6 +289,7 @@ export class TableDesignerWebviewController extends ReactWebviewPanelController<
},
};
await this._untitledSqlDocumentService.newQuery(script);
await UserSurvey.getInstance().promptUserForNPSFeedback();
return state;
});