Skip to content

Commit

Permalink
feat: teams manifest tool (microsoft#6581)
Browse files Browse the repository at this point in the history
* Added Teams manifest type defenition and default value

* Added Teams manifest modal

* Fix defailt manifest

* - Add download manifest icon
- Fix manifest generation to use appId from publish profile

* Add show manifest link

* removing unused ref

* Fix guid generation

* Fixing icon styling

* Making requested PR changes

* Fixing front end error handling

* fixing potentiall undefined param passing

* Adding aka.ms link for teams app studio deep link

Co-authored-by: Patrick Volum <[email protected]>
Co-authored-by: Soroush <[email protected]>
  • Loading branch information
3 people authored Apr 2, 2021
1 parent 5b374b0 commit 427fe29
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 3 deletions.
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[];
};

0 comments on commit 427fe29

Please sign in to comment.