From a16e34e618377a75aaa5689582c2ad1a202d7cbb Mon Sep 17 00:00:00 2001 From: Trevor Harris Date: Tue, 2 Jul 2024 11:58:42 -0700 Subject: [PATCH 1/5] Proposal for a new private API called 'associatedApps' --- .../teams-js/src/private/associatedApps.ts | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 packages/teams-js/src/private/associatedApps.ts diff --git a/packages/teams-js/src/private/associatedApps.ts b/packages/teams-js/src/private/associatedApps.ts new file mode 100644 index 0000000000..37a887d2be --- /dev/null +++ b/packages/teams-js/src/private/associatedApps.ts @@ -0,0 +1,205 @@ +import { sendMessageToParentAsync } from '../internal/communication'; +import { ensureInitialized } from '../internal/internalAPIs'; +import { ErrorCode, TabInstance } from '../public'; +import { runtime } from '../public/runtime'; + +// Open questions +// 1. I've re-used `TabInstance` from the public API, does that contain all of the information you and app developers might need? +// 2. I didn't see any reason to add a `getTabs` function because `pages.tabs.getTabInstances`. Any reason that won't work for you? +// 3. I've added an `AppTypes[]` param to `addAndConfigureApp` to allow for the host to show different app types to the user. Very open to changes. +// 4. I've added empty, private `validate` functions for the threadId and TabInstance. Any validation that is possible will help prevent against +// bad data being sent to the host. If you have any validation that can be done, please add it there. If you *can* use restrictive types like UUID +// or something, that would be even better. + +// TODO: Add unit tests +// TODO: Add E2E tests + +/** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + */ +export namespace associatedApps { + export enum AppTypes { + meeting = 'meeting', + } + + export namespace tab { + interface ErrorResponse { + errorCode: ErrorCode; + message?: string; // TODO: Can remove if you don't have a message to send back to the app developer + } + + /** + * TODO: Add full description of what this function does, ie "Launches host-owned UI that lets a user select an app, installs it if required, + * runs through app configuration if required, and then associates the app with the threadId provided. If external docs exist, link to them here" + * + * @param threadId Info about where this comes from, links to external docs if available, etc. + * @param appTypes what type of applications to show the user + * + * @returns The TabInstance of the newly associated app + * + * @throws TODO: Description of errors that can be thrown from this function + */ + export function addAndConfigureApp(threadId: string, appTypes: AppTypes[]): Promise { + ensureInitialized(runtime); // TODO: add frameContext checks if this is limited to certain contexts such as content + + if (!isSupported()) { + throw new Error(ErrorCode.NOT_SUPPORTED_ON_PLATFORM.toString()); + } + + validateThreadId(threadId); + + return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly + 'associatedApps.tab.addAndConfigureApp', + [threadId, appTypes], + ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + if (!wasSuccessful) { + // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw + const error = response as ErrorResponse; + throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); + } + return response as TabInstance; + }); + } + + /** + * TODO: Add full description of what this function does, ie "Allows the user to go through the tab config process again for the specified app. If + * no config process exists, X happens, etc." + * + * @param tab fill in details + * @param threadId Info about where this comes from, links to external docs if available, etc. + * + * @returns The TabInstance of the newly configured app + * + * @throws TODO: Description of errors that can be thrown from this function + */ + export function reconfigure(tab: TabInstance, threadId: string): Promise { + ensureInitialized(runtime); // TODO: add frameContext checks if this is limited to certain contexts such as content + + if (!isSupported()) { + throw new Error(ErrorCode.NOT_SUPPORTED_ON_PLATFORM.toString()); + } + + validateTab(tab); + validateThreadId(threadId); + + return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly + 'associatedApps.tab.reconfigure', + [tab, threadId], + ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + if (!wasSuccessful) { + // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw + const error = response as ErrorResponse; + throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); + } + return response as TabInstance; + }); + } + + /** + * TODO: Add full description of what this function does, ie "Renames the tab associated with an app" + * + * @param tab fill in details + * @param threadId Info about where this comes from, links to external docs if available, etc. + * + * @returns The TabInstance of the newly renamed app + * + * @throws TODO: Description of errors that can be thrown from this function + */ + export function rename(tab: TabInstance, threadId: string): Promise { + ensureInitialized(runtime); // TODO: add frameContext checks if this is limited to certain contexts such as content + + if (!isSupported()) { + throw new Error(ErrorCode.NOT_SUPPORTED_ON_PLATFORM.toString()); + } + + validateTab(tab); + validateThreadId(threadId); + + return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly + 'associatedApps.tab.rename', + [tab, threadId], + ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + if (!wasSuccessful) { + // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw + const error = response as ErrorResponse; + throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); + } + return response as TabInstance; + }); + } + + /** + * TODO: Add full description of what this function does, ie "Removes a tab associated with an app, must be called on an app tab, etc." + * + * @param tab fill in details + * @param threadId Info about where this comes from, links to external docs if available, etc. + * + * @throws TODO: Description of errors that can be thrown from this function + */ + export function remove(tab: TabInstance, threadId: string): Promise { + ensureInitialized(runtime); // TODO: add frameContext checks if this is limited to certain contexts such as content + + if (!isSupported()) { + throw new Error(ErrorCode.NOT_SUPPORTED_ON_PLATFORM.toString()); + } + + validateTab(tab); + validateThreadId(threadId); + + return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly + 'associatedApps.tab.remove', + [tab, threadId], + ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + if (!wasSuccessful) { + // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw + const error = response as ErrorResponse; + throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); + } + }); + } + + /** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + */ + export function isSupported(): boolean { + throw new Error('Not implemented'); + } + + function validateThreadId(threadId: string) { + // TODO: Any checks you can do on threadId to guarantee valid (not null, not empty, not undefined, format if possible, etc.) + /* + if (threadId is not valid) { + throw new Error(`${threadId} is not a valid threadId`); + } + */ + } + + function validateTab(tabInstance: TabInstance) { + // TODO: Any checks you can do on TabInstance to guarantee valid (not null, not empty, not undefined, all required properties set to legal values, etc.) + /* + if (tabInstance is not valid) { + throw new Error(`TabInstance ${tabInstance.internalTabInstanceId} is not a valid, extra detail if available`); + } + */ + } + } + /** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + */ + export function isSupported(): boolean { + throw new Error('Not implemented'); + } +} From a6102ca77c6a8f21966ecfdc00ee31551ba875e6 Mon Sep 17 00:00:00 2001 From: Trevor Harris Date: Tue, 2 Jul 2024 12:02:16 -0700 Subject: [PATCH 2/5] Adding more comments --- .../teams-js/src/private/associatedApps.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/teams-js/src/private/associatedApps.ts b/packages/teams-js/src/private/associatedApps.ts index 37a887d2be..068f4fd4bb 100644 --- a/packages/teams-js/src/private/associatedApps.ts +++ b/packages/teams-js/src/private/associatedApps.ts @@ -10,6 +10,8 @@ import { runtime } from '../public/runtime'; // 4. I've added empty, private `validate` functions for the threadId and TabInstance. Any validation that is possible will help prevent against // bad data being sent to the host. If you have any validation that can be done, please add it there. If you *can* use restrictive types like UUID // or something, that would be even better. +// 5. I've made the namespace structure an empty `associatedApps` namespace that only contains the `tab` namespace. This was an attempt to leave room for +// expansion in the future for non-tab scenarios that will make it less likely that your callers will need to update their code. Open to opinions though. // TODO: Add unit tests // TODO: Add E2E tests @@ -19,12 +21,22 @@ import { runtime } from '../public/runtime'; * @internal * @beta * Limited to Microsoft-internal use + * + * TODO: Brief description of what this capability does */ export namespace associatedApps { export enum AppTypes { meeting = 'meeting', } + /** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + * + * TODO: Brief description of what this capability does + */ export namespace tab { interface ErrorResponse { errorCode: ErrorCode; @@ -32,6 +44,11 @@ export namespace associatedApps { } /** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + * * TODO: Add full description of what this function does, ie "Launches host-owned UI that lets a user select an app, installs it if required, * runs through app configuration if required, and then associates the app with the threadId provided. If external docs exist, link to them here" * @@ -66,6 +83,11 @@ export namespace associatedApps { } /** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + * * TODO: Add full description of what this function does, ie "Allows the user to go through the tab config process again for the specified app. If * no config process exists, X happens, etc." * @@ -101,6 +123,11 @@ export namespace associatedApps { } /** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + * * TODO: Add full description of what this function does, ie "Renames the tab associated with an app" * * @param tab fill in details @@ -135,6 +162,11 @@ export namespace associatedApps { } /** + * @hidden + * @internal + * @beta + * Limited to Microsoft-internal use + * * TODO: Add full description of what this function does, ie "Removes a tab associated with an app, must be called on an app tab, etc." * * @param tab fill in details From deec7f7b1a6ceb0a466dbf0591282af9afd929f8 Mon Sep 17 00:00:00 2001 From: Trevor Harris Date: Tue, 2 Jul 2024 16:08:02 -0700 Subject: [PATCH 3/5] PR feedback --- .../teams-js/src/private/associatedApps.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/teams-js/src/private/associatedApps.ts b/packages/teams-js/src/private/associatedApps.ts index 068f4fd4bb..a8264258ab 100644 --- a/packages/teams-js/src/private/associatedApps.ts +++ b/packages/teams-js/src/private/associatedApps.ts @@ -1,6 +1,6 @@ import { sendMessageToParentAsync } from '../internal/communication'; import { ensureInitialized } from '../internal/internalAPIs'; -import { ErrorCode, TabInstance } from '../public'; +import { ErrorCode, SdkError, TabInstance } from '../public'; import { runtime } from '../public/runtime'; // Open questions @@ -38,10 +38,10 @@ export namespace associatedApps { * TODO: Brief description of what this capability does */ export namespace tab { - interface ErrorResponse { - errorCode: ErrorCode; - message?: string; // TODO: Can remove if you don't have a message to send back to the app developer - } + // interface ErrorResponse { + // errorCode: ErrorCode; + // message?: string; // TODO: Can remove if you don't have a message to send back to the app developer + // } /** * @hidden @@ -68,14 +68,14 @@ export namespace associatedApps { validateThreadId(threadId); - return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + return sendMessageToParentAsync<[boolean, TabInstance | SdkError]>( 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly 'associatedApps.tab.addAndConfigureApp', [threadId, appTypes], - ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + ).then(([wasSuccessful, response]: [boolean, TabInstance | SdkError]) => { if (!wasSuccessful) { // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw - const error = response as ErrorResponse; + const error = response as SdkError; throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); } return response as TabInstance; @@ -108,14 +108,14 @@ export namespace associatedApps { validateTab(tab); validateThreadId(threadId); - return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + return sendMessageToParentAsync<[boolean, TabInstance | SdkError]>( 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly 'associatedApps.tab.reconfigure', [tab, threadId], - ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + ).then(([wasSuccessful, response]: [boolean, TabInstance | SdkError]) => { if (!wasSuccessful) { // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw - const error = response as ErrorResponse; + const error = response as SdkError; throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); } return response as TabInstance; @@ -133,7 +133,7 @@ export namespace associatedApps { * @param tab fill in details * @param threadId Info about where this comes from, links to external docs if available, etc. * - * @returns The TabInstance of the newly renamed app + * @returns The TabInstance of the newly renamed app tab * * @throws TODO: Description of errors that can be thrown from this function */ @@ -147,14 +147,14 @@ export namespace associatedApps { validateTab(tab); validateThreadId(threadId); - return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + return sendMessageToParentAsync<[boolean, TabInstance | SdkError]>( 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly 'associatedApps.tab.rename', [tab, threadId], - ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + ).then(([wasSuccessful, response]: [boolean, TabInstance | SdkError]) => { if (!wasSuccessful) { // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw - const error = response as ErrorResponse; + const error = response as SdkError; throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); } return response as TabInstance; @@ -184,14 +184,14 @@ export namespace associatedApps { validateTab(tab); validateThreadId(threadId); - return sendMessageToParentAsync<[boolean, TabInstance | ErrorResponse]>( + return sendMessageToParentAsync<[boolean, TabInstance | SdkError]>( 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly 'associatedApps.tab.remove', [tab, threadId], - ).then(([wasSuccessful, response]: [boolean, TabInstance | ErrorResponse]) => { + ).then(([wasSuccessful, response]: [boolean, SdkError]) => { if (!wasSuccessful) { // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw - const error = response as ErrorResponse; + const error = response as SdkError; throw new Error(`Error code: ${error.errorCode}, message: ${error.message ?? 'None'}`); } }); From 1a3e9e818ae657cf770cb7d9d154440acb59185e Mon Sep 17 00:00:00 2001 From: Trevor Harris Date: Wed, 3 Jul 2024 08:37:22 -0700 Subject: [PATCH 4/5] PR feedback from Helen --- packages/teams-js/src/private/associatedApps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/teams-js/src/private/associatedApps.ts b/packages/teams-js/src/private/associatedApps.ts index a8264258ab..2748a8ab90 100644 --- a/packages/teams-js/src/private/associatedApps.ts +++ b/packages/teams-js/src/private/associatedApps.ts @@ -59,7 +59,7 @@ export namespace associatedApps { * * @throws TODO: Description of errors that can be thrown from this function */ - export function addAndConfigureApp(threadId: string, appTypes: AppTypes[]): Promise { + export function addAndConfigure(threadId: string, appTypes: AppTypes[]): Promise { ensureInitialized(runtime); // TODO: add frameContext checks if this is limited to certain contexts such as content if (!isSupported()) { From 08a6610e1b1275c576bbe5117fc623ce5d6fd0b2 Mon Sep 17 00:00:00 2001 From: Trevor Harris Date: Tue, 9 Jul 2024 09:26:13 -0700 Subject: [PATCH 5/5] Updated based on offline discussion with Helen and Debo --- .../{associatedApps.ts => hostEntity.ts} | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) rename packages/teams-js/src/private/{associatedApps.ts => hostEntity.ts} (87%) diff --git a/packages/teams-js/src/private/associatedApps.ts b/packages/teams-js/src/private/hostEntity.ts similarity index 87% rename from packages/teams-js/src/private/associatedApps.ts rename to packages/teams-js/src/private/hostEntity.ts index 2748a8ab90..f96ac9c5a8 100644 --- a/packages/teams-js/src/private/associatedApps.ts +++ b/packages/teams-js/src/private/hostEntity.ts @@ -4,14 +4,12 @@ import { ErrorCode, SdkError, TabInstance } from '../public'; import { runtime } from '../public/runtime'; // Open questions -// 1. I've re-used `TabInstance` from the public API, does that contain all of the information you and app developers might need? +// 1. According to Debo, `TabInstance` from the public API looks like it would work. Helen asked Debo to follow up about getting more recent fields added. // 2. I didn't see any reason to add a `getTabs` function because `pages.tabs.getTabInstances`. Any reason that won't work for you? -// 3. I've added an `AppTypes[]` param to `addAndConfigureApp` to allow for the host to show different app types to the user. Very open to changes. +// 3. I've added an `AppTypes[]` param to `addAndConfigureApp` to allow for the host to show different app types to the user. Helen going to see if there are more types to add here to start. // 4. I've added empty, private `validate` functions for the threadId and TabInstance. Any validation that is possible will help prevent against // bad data being sent to the host. If you have any validation that can be done, please add it there. If you *can* use restrictive types like UUID // or something, that would be even better. -// 5. I've made the namespace structure an empty `associatedApps` namespace that only contains the `tab` namespace. This was an attempt to leave room for -// expansion in the future for non-tab scenarios that will make it less likely that your callers will need to update their code. Open to opinions though. // TODO: Add unit tests // TODO: Add E2E tests @@ -22,9 +20,10 @@ import { runtime } from '../public/runtime'; * @beta * Limited to Microsoft-internal use * - * TODO: Brief description of what this capability does + * TODO: Brief description of what this capability does. For example: + * This capability allows an app to associate other apps with a host entity, such as a Teams channel or chat, and configure them as needed. */ -export namespace associatedApps { +export namespace hostEntity { export enum AppTypes { meeting = 'meeting', } @@ -35,14 +34,10 @@ export namespace associatedApps { * @beta * Limited to Microsoft-internal use * - * TODO: Brief description of what this capability does + * TODO: Brief description of what this capability does. For example: + * This capability allows an app to associate other tab apps with a host entity, such as a Teams channel or chat, and configure them as needed. */ export namespace tab { - // interface ErrorResponse { - // errorCode: ErrorCode; - // message?: string; // TODO: Can remove if you don't have a message to send back to the app developer - // } - /** * @hidden * @internal @@ -52,26 +47,28 @@ export namespace associatedApps { * TODO: Add full description of what this function does, ie "Launches host-owned UI that lets a user select an app, installs it if required, * runs through app configuration if required, and then associates the app with the threadId provided. If external docs exist, link to them here" * - * @param threadId Info about where this comes from, links to external docs if available, etc. + * @param hostEntityId Info about where this value comes from, links to external docs if available, etc. For example: + * The id of the host entity that your app wants to associate another app with. In Teams this would be the threadId + * * @param appTypes what type of applications to show the user * * @returns The TabInstance of the newly associated app * * @throws TODO: Description of errors that can be thrown from this function */ - export function addAndConfigure(threadId: string, appTypes: AppTypes[]): Promise { + export function addAndConfigure(hostEntityId: string, appTypes: AppTypes[]): Promise { ensureInitialized(runtime); // TODO: add frameContext checks if this is limited to certain contexts such as content if (!isSupported()) { throw new Error(ErrorCode.NOT_SUPPORTED_ON_PLATFORM.toString()); } - validateThreadId(threadId); + validateThreadId(hostEntityId); return sendMessageToParentAsync<[boolean, TabInstance | SdkError]>( 'apiVersionTag', // TODO: see uses of getApiVersionTag in other files to do this correctly 'associatedApps.tab.addAndConfigureApp', - [threadId, appTypes], + [hostEntityId, appTypes], ).then(([wasSuccessful, response]: [boolean, TabInstance | SdkError]) => { if (!wasSuccessful) { // TODO: Can handle error codes differently here, for example if you don't want "user cancelled" to throw