Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { v4 as uuidv4 } from 'uuid';
import { jsx } from '@emotion/core';
import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import formatMessage from 'format-message';
import { Text } from 'office-ui-fabric-react/lib/Text';
import { IButtonStyles, IconButton } from 'office-ui-fabric-react/lib/components/Button';
import { FontSizes, NeutralColors } from '@uifabric/fluent-theme/lib/fluent';
import { useRef } from 'react';
import { ITextField, TextField } from 'office-ui-fabric-react/lib/components/TextField';
import { DialogTypes, DialogWrapper } from '@bfc/ui-shared/lib/components/DialogWrapper';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { useRecoilValue } from 'recoil';

import { defaultTeamsManifest } from '../../constants';
import { dispatcherState } from '../../recoilModel';

const iconButtonStyle: IButtonStyles = {
root: {
height: 'unset',
float: 'right',
marginRight: '10px',
},
menuIcon: {
backgroundColor: NeutralColors.white,
color: NeutralColors.gray130,
fontSize: FontSizes.size16,
},
rootDisabled: {
backgroundColor: NeutralColors.white,
},
rootHovered: {
backgroundColor: 'unset',
},
rootPressed: {
backgroundColor: 'unset',
},
};

const teamsAppStudioDeepLink = 'https://aka.ms/AppStudioTeamsLink';

type TeamsManifestGeneratorModalProps = {
hidden: boolean;
botAppId: string;
botDisplayName: string;
onDismiss: () => void;
};

export const TeamsManifestGeneratorModal = (props: TeamsManifestGeneratorModalProps) => {
const textFieldRef = useRef<ITextField>(null);
const { setApplicationLevelError } = useRecoilValue(dispatcherState);

const copyCodeToClipboard = () => {
try {
if (textFieldRef.current) {
textFieldRef.current.select();
document.execCommand('copy');
textFieldRef.current.setSelectionRange(0, 0);
textFieldRef.current.blur();
}
} catch (e) {
setApplicationLevelError(e);
}
};

const generateTeamsManifest = () => {
const appId = props.botAppId ? props.botAppId : '{AddBotAppId}';
const botName = props.botDisplayName ? props.botDisplayName : '{AddBotDisplayName}';
const result = defaultTeamsManifest;
result.id = uuidv4().toString();
result.description.short = `${formatMessage('short description for')} ${botName}`;
result.description.full = `${formatMessage('full description for')} ${botName}`;
result.packageName = botName;
result.name.short = botName;
result.name.full = botName;
result.bots[0].botId = appId;
return JSON.stringify(defaultTeamsManifest, null, 2);
};

return (
<DialogWrapper
dialogType={DialogTypes.CreateFlow}
isOpen={!props.hidden}
subText={formatMessage(
'Your Teams adapter is configured for your published bot. Copy the manifest, open App Studio in Teams and add the manifest so you can test your bot in Teams'
)}
title={formatMessage('Teams Manifest')}
onDismiss={props.onDismiss}
>
<div>
<Text style={{ fontWeight: 700 }}>{formatMessage('Teams manifest for your bot:')}</Text>
<IconButton
ariaLabel={formatMessage('Download Icon')}
download={'teamsManifest.json'}
href={'data:text/plain;charset=utf-8,' + encodeURIComponent(generateTeamsManifest())}
menuIconProps={{ iconName: 'Download' }}
styles={iconButtonStyle}
/>
<IconButton
ariaLabel={formatMessage('Copy Icon')}
menuIconProps={{ iconName: 'Copy' }}
styles={iconButtonStyle}
onClick={() => {
copyCodeToClipboard();
}}
/>
</div>
<TextField
multiline
componentRef={textFieldRef}
rows={25}
styles={{ root: { marginTop: '10px' }, fieldGroup: { backgroundColor: '#f3f2f1' } }}
value={generateTeamsManifest()}
/>
<DialogFooter>
<DefaultButton
text={formatMessage('Close')}
onClick={() => {
props.onDismiss();
}}
/>
<PrimaryButton href={teamsAppStudioDeepLink} target="_blank" text={formatMessage('Open Teams')} />
</DialogFooter>
</DialogWrapper>
);
};
37 changes: 36 additions & 1 deletion Composer/packages/client/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { FeedName } from '@botframework-composer/types/src';
import { FeedName, TeamsManifest } from '@botframework-composer/types/src';
import formatMessage from 'format-message';
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';

Expand Down Expand Up @@ -421,3 +421,38 @@ export const runtimeOptions: IDropdownOption[] = [
];

export const onboardingDisabled = false;

export const defaultTeamsManifest: TeamsManifest = {
$schema: 'https://developer.microsoft.com/en-us/json-schemas/teams/v1.9/MicrosoftTeams.schema.json',
manifestVersion: '1.9',
version: '1.0.0',
id: '',
packageName: '',
developer: {
name: 'contoso',
websiteUrl: 'https://contoso.com',
privacyUrl: 'https://cotoso.com/privacy',
termsOfUseUrl: 'https://contoso.com/terms',
},
icons: {
color: '',
outline: '',
},
name: {
short: '',
full: '',
},
description: {
short: '',
full: '',
},
accentColor: '#FFFFFF',
bots: [
{
botId: '',
scopes: ['personal'],
},
],
permissions: ['identity', 'messageTeamMembers'],
validDomains: ['token.botframework.com'],
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { OpenConfirmModal } from '@bfc/ui-shared';
import TelemetryClient from '../../../telemetry/TelemetryClient';
import { LoadingSpinner } from '../../../components/LoadingSpinner';
import { navigateTo } from '../../../utils/navigation';
import { settingsState } from '../../../recoilModel';
import { botDisplayNameState, settingsState } from '../../../recoilModel';
import { AuthClient } from '../../../utils/authClient';
import { AuthDialog } from '../../../components/Auth/AuthDialog';
import { armScopes } from '../../../constants';
Expand All @@ -40,6 +40,7 @@ import {
errorTextStyle,
columnSizes,
} from '../styles';
import { TeamsManifestGeneratorModal } from '../../../components/Adapters/TeamsManifestGeneratorModal';

import ABSChannelSpeechModal from './ABSChannelSpeechModal';

Expand All @@ -62,6 +63,7 @@ type AzureResourcePointer = {
alternateSubscriptionId?: string | undefined;
resourceName: string;
resourceGroupName: string;
microsoftAppId: string;
};

type AzureChannelStatus = {
Expand All @@ -86,12 +88,14 @@ export const ABSChannels: React.FC<RuntimeSettingsProps> = (props) => {
const [currentResource, setCurrentResource] = useState<AzureResourcePointer | undefined>();
const [channelStatus, setChannelStatus] = useState<AzureChannelsStatus | undefined>();
const { publishTargets } = useRecoilValue(settingsState(projectId));
const botDisplayName = useRecoilValue(botDisplayNameState(projectId));
const [token, setToken] = useState<string | undefined>();
const [availableSubscriptions, setAvailableSubscriptions] = useState<Subscription[]>([]);
const [publishTargetOptions, setPublishTargetOptions] = useState<IDropdownOption[]>([]);
const [isLoading, setLoadingStatus] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
const [showSpeechModal, setShowSpeechModal] = useState<boolean>(false);
const [showTeamsManifestModal, setShowTeamsManifestModal] = useState<boolean>(false);
const { setApplicationLevelError } = useRecoilValue(dispatcherState);
/* Copied from Azure Publishing extension */
const getSubscriptions = async (token: string): Promise<Array<Subscription>> => {
Expand Down Expand Up @@ -134,6 +138,7 @@ export const ABSChannels: React.FC<RuntimeSettingsProps> = (props) => {
if (profile) {
const config = JSON.parse(profile.configuration);
setCurrentResource({
microsoftAppId: config?.settings?.MicrosoftAppId,
resourceName: config.name,
resourceGroupName: config.name,
subscriptionId: config.subscriptionId,
Expand Down Expand Up @@ -241,7 +246,9 @@ export const ABSChannels: React.FC<RuntimeSettingsProps> = (props) => {
};
}
await httpClient.put(url, data, { headers: { Authorization: `Bearer ${token}` } });

if (channelId === CHANNELS.TEAMS) {
setShowTeamsManifestModal(true);
}
// success!!
setChannelStatus({
...channelStatus,
Expand Down Expand Up @@ -494,6 +501,18 @@ export const ABSChannels: React.FC<RuntimeSettingsProps> = (props) => {
<Spinner />
</Stack.Item>
)}
{key === CHANNELS.TEAMS && channelStatus?.[key].enabled && !channelStatus?.[key].loading && (
<Stack.Item>
<Link
styles={{ root: { marginTop: '7px' } }}
onClick={() => {
setShowTeamsManifestModal(true);
}}
>
{formatMessage('Open Manifest')}
</Link>
</Stack.Item>
)}
</Stack>
);

Expand Down Expand Up @@ -581,6 +600,14 @@ export const ABSChannels: React.FC<RuntimeSettingsProps> = (props) => {
{absTableRow(CHANNELS.SPEECH, formatMessage('Speech'), speechHelpLink)}
</Fragment>
)}
<TeamsManifestGeneratorModal
botAppId={currentResource?.microsoftAppId ? currentResource.microsoftAppId : ''}
botDisplayName={botDisplayName}
hidden={!showTeamsManifestModal}
onDismiss={() => {
setShowTeamsManifestModal(false);
}}
/>
</div>
</React.Fragment>
);
Expand Down
58 changes: 58 additions & 0 deletions Composer/packages/types/src/creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,61 @@ export const nodeFeedKey = 'firstPartyNode';
export const defaultFeeds = [nodeFeedKey, csharpFeedKey] as const;
export type FeedName = typeof defaultFeeds[number];
export type FeedType = 'npm' | 'nuget';

type WebApplicationInfo = {
id: string;
resource: string;
};

type Command = {
title: string;
description: string;
};

type Name = {
short: string;
full: string;
};

type Icons = {
color: string;
outline: string;
};

type Developer = {
name: string;
websiteUrl: string;
privacyUrl: string;
termsOfUseUrl: string;
};

type CommandList = {
scopes: string[];
commands: Command[];
};

type Bot = {
botId: string;
scopes: string[];
commandList?: CommandList[];
supportsFiles?: boolean;
isNotificationOnly?: boolean;
};

export type TeamsManifest = {
$schema: string;
manifestVersion: string;
version: string;
id: string;
packageName: string;
developer: Developer;
icons: Icons;
name: Name;
description: Name;
accentColor: string;
bots: Bot[];
permissions: string[];
validDomains: string[];
webApplicationInfo?: WebApplicationInfo;
devicePermissions?: string[];
};