Fix Field Visit Tab sample, add new Tab Config sample (#1088)

* Adding react-teams-tab-field-visit-mashup sample

* Update to new (still Beta) Graph call to post msg

* Update readme w/telemetry merged

* Generated web part

* Quick UI update

* Now configures a tab!

* Updated property panel for multiple tab handling

* Roughed out new tab name properties

* Added multi-tab property handling

* Roughed out tab picking

* Added link selection to UI

* Clean up UI, property validation

* Actually works configuring tabs

* Split out React components

* Now handles redirect query

* Localized redirect message

* Added documentation
This commit is contained in:
Bob German 2020-01-09 04:09:14 -05:00 committed by Vesa Juvonen
parent 66918a283b
commit 6ae535f57d
38 changed files with 18996 additions and 32 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.9.1",
"libraryName": "react-teams-configure-tab",
"libraryId": "dd64a2db-1a14-486a-88bf-e3554ae37185",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,95 @@
## Teams Configuration Tab
This web part makes a modern SharePoint page into a Teams tab configuration page for use in a Teams application. This allows low-code developers to create Teams applications containing configurable tabs without the need to code a custom configuration page. Using this tool, along with Microsoft Teams App Studio, low-code developers can build Teams applications entirely from modern SharePoint pages.
Microsoft Teams applications support _configurable tabs_ that work in Teams channels and group chat conversations. Configurable tabs require a configuation page that is presented to users when they add a tab. This web part implements a simple configuration experience.
![Tab configuration](documentation/images/SPTabAppStudioTeamsTab006.png)
When the user selects one of the tab options, the tab is saved pointing to the corresponding SharePoint page.
![Tab configuration](documentation/images/SPTabAppStudioTeamsTab007.png)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.9.1-green.svg)
## Applies to
* [SharePoint Framework Developer](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
Tab Configuration Web Part | Bob German ([@Bob1German](http://www.twitter.com/Bob1German))
## Version history
Version|Date|Comments
-------|----|--------
1.0|January 6, 2020|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
#### 1. Build the web part
Build and package the web part
* npm install
* gulp bundle --ship
* gulp package-solution --ship
Install into your SharePoint app catalog and add it to a SharePoint site.
#### 2. Create a configuration page
Create a new modern SharePoint page that will become the tab configuration page, and add the Configure Tab web part. Edit the web part to set up the tab information.
![Tab configuration page](documentation/images/SPConfigPageCallouts.png)
Your configuration page will present users with one or more choices for tabs they'd like to show. Enter a line for each choice into the edior panel:
(1) Enter a name for each tab, each on its own line
(2) Enter a unique entity ID for each tab, each on its own line corresponding to the lines in (1)
(3) Enter the URL for each tab, again each on its own line
If you want all tab clicks to be redirected back through this page, select the Redirect checkbox. This allows you to change the URL's for tabs even after they've been configured, however users may notice the page flickering as they are redirected.
#### 3. Set up the app manifest
Create a new app manifest in [Teams App Studio](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/app-studio-overview), and add a Team Tab. Paste the URL of your configuration page into the Configuration URL field.
![Create a Team Tab in App Studio](documentation/images/SPTabAppStudioTeamsTab001.png)
If you want users to be able to edit the tab and return to the configuration page (and maybe switch to another choice), then check the Can update configuration checkbox. You can also decide if your app is available in Teams, Group Chats, or both.
#### 4. Deploy
Now check to be sure the valid domains are set up (as above) and install the app into a Teams channel or Group Chat. You will be presentd with the configuration page.
![Tab configuration](documentation/images/SPTabAppStudioTeamsTab006.png)
If you only configured one choice, just click the Save button and save your tab. If you configured multiple choices, pick one and the save button will light up and allow you to save the tab.
![Tab configuration](documentation/images/SPTabAppStudioTeamsTab007.png)
Now your page is visible within the new tab for all to see.
#### 5. Maintain
If you set the web part up to redirect tab clicks, it saved its own URL and the entity ID into Teams for each tab. The web part will look at the entity ID and redirect to the target page. Thus it is possible to edit the web part and change the URLs, and thus change all the tabs that had previously been created.
If you edit the web part and remove the choice (entity ID), it will prompt the user to edit the tab and make another selection.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-teams-tab-field-visit-mashup" />

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"configure-tab-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/configureTab/ConfigureTabWebPart.js",
"manifest": "./src/webparts/configureTab/ConfigureTabWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ConfigureTabWebPartStrings": "lib/webparts/configureTab/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-teams-configure-tab",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-teams-configure-tab-client-side-solution",
"id": "dd64a2db-1a14-486a-88bf-e3554ae37185",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-teams-configure-tab.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "react-teams-configure-tab",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.9.1",
"@microsoft/sp-lodash-subset": "1.9.1",
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
"@microsoft/sp-webpart-base": "1.9.1",
"@microsoft/teams-js": "^1.5.2",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.9.1",
"@microsoft/sp-tslint-rules": "1.9.1",
"@microsoft/sp-module-interfaces": "1.9.1",
"@microsoft/sp-webpart-workbench": "1.9.1",
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "f4190cf7-c108-4e8f-8173-a092056b5b0f",
"alias": "ConfigureTabWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "ConfigureTab" },
"description": { "default": "Place this web part to create a Teams configuration page that points to the content page of your choice" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "ConfigureTab"
}
}]
}

View File

@ -0,0 +1,140 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, UrlQueryParameterCollection } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneCheckbox,
PropertyPaneLabel
} from '@microsoft/sp-webpart-base';
import * as strings from 'ConfigureTabWebPartStrings';
import { ConfigureTab, IConfigureTabProps } from './components/ConfigureTab';
import { RedirectTab, IRedirectTabProps } from './components/RedirectTab';
import { TabError, ITabErrorProps } from './components/TabError';
import { ITabLink } from './model/ITabLink';
import TeamsConfigurationService from './services/TeamsConfigurationService';
import TabLinkParser from './services/TabLinkParser';
export interface IConfigureTabWebPartProps {
tabNames: string;
entityIds: string;
contentPageUrls: string;
redirectPages: boolean;
}
export default class ConfigureTabWebPart extends BaseClientSideWebPart<IConfigureTabWebPartProps> {
private teamsConfigurationService = new TeamsConfigurationService();
private tabLinkParser = new TabLinkParser();
public render(): void {
var tabLinkChoices: ITabLink[] = null;
try {
tabLinkChoices = this.tabLinkParser.parseTabLinks(this.properties.tabNames, this.properties.entityIds, this.properties.contentPageUrls);
var queryParms = new UrlQueryParameterCollection(window.location.href);
var redirectEntityId = queryParms.getValue("entityId");
if (!redirectEntityId) {
// We are in configuration mode, running on as a Teams tab configuration page
// Allow user to configure a tab
const element: React.ReactElement<IConfigureTabProps> = React.createElement(
ConfigureTab,
{
tabLinkChoices: tabLinkChoices,
message: "",
tabLinkSelected: ((item: ITabLink) => {
this.teamsConfigurationService.configureTab(item, this.properties.redirectPages);
})
}
);
ReactDom.render(element, this.domElement);
} else {
// We are in redirect mode, redirecting a request for a content page
// Redirect to the named tab
const element: React.ReactElement<IRedirectTabProps> = React.createElement(
RedirectTab,
{
tabLinkChoices: tabLinkChoices,
entityId: redirectEntityId
}
);
ReactDom.render(element, this.domElement);
}
}
catch (error) {
// Display error
const element: React.ReactElement<ITabErrorProps> = React.createElement(
TabError,
{
message: error,
}
);
ReactDom.render(element, this.domElement);
}
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneLabel('', {
text: strings.TabInstructions
}),
PropertyPaneTextField('tabNames', {
label: strings.TabNamesFieldLabel,
multiline: true,
rows: 5
}),
PropertyPaneTextField('entityIds', {
label: strings.EntityIdsFieldLabel,
multiline: true,
rows: 5
}),
PropertyPaneTextField('contentPageUrls', {
label: strings.ContentPageUrlsFieldLabel,
multiline: true,
rows: 5
}),
PropertyPaneCheckbox('redirectPages', {
text: strings.RedirectFieldLabel
}),
PropertyPaneLabel('', {
text: strings.RedirectFieldInstructions
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,89 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.configureTab {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: #038387;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.tabChoice {
padding: 2px 0px 10px 5px;
}
.tabName {
@include ms-font-xl;
}
.tabLink {
@include ms-font-xs;
@include ms-fontColor-white;
a:link, a:visited {
color: $ms-color-white;
}
}
.selected {
background-color: #005b70;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: #005b70;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,118 @@
import * as React from 'react';
import styles from './ConfigureTab.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';
import { ITabLink } from '../model/ITabLink';
import * as strings from 'ConfigureTabWebPartStrings';
export interface IConfigureTabProps {
tabLinkChoices?: ITabLink[];
message: string;
tabLinkSelected?: (ITabLink) => void;
}
interface IConfigureTabState {
selectedTabLink?: ITabLink;
}
export class ConfigureTab extends React.Component<IConfigureTabProps, IConfigureTabState> {
constructor(props: IConfigureTabProps) {
super(props);
this.state = {
selectedTabLink: null
};
}
public render(): React.ReactElement<IConfigureTabProps> {
if (this.props.tabLinkChoices && this.props.tabLinkChoices.length > 1) {
// We have multiple tab link choices; allow the user to pick one
return (
<div className={styles.configureTab}>
<div className={styles.container}>
<div className={styles.row}>
<span className={styles.title}>{strings.PleaseSelectHeading}</span>
<ul className={styles.column}>
{this.props.tabLinkChoices.map((item) => {
if (this.state.selectedTabLink &&
item.entityId == this.state.selectedTabLink.entityId) {
// Display selected tab
return (
<li className={styles.selected + ' ' + styles.tabChoice}>
<span className = {styles.tabName}>{item.tabName}</span>
<br />
<span className={styles.tabLink}>
<a href={escape(item.contentPageUrl)}>{escape(item.contentPageUrl)}</a>
</span>
</li>
);
} else {
// Display unselected tab
return (
<li className={styles.tabChoice}
onClick={() => {
this.setState({
selectedTabLink: item
});
this.props.tabLinkSelected(item);
}} >
<span className = {styles.tabName}>{item.tabName}</span>
<br />
<span className={styles.tabLink}>
<a href={escape(item.contentPageUrl)}>{escape(item.contentPageUrl)}</a>
</span>
</li>
);
}
})}
<p className={styles.subTitle}>{escape(this.props.message)}</p>
</ul>
</div>
</div>
</div>
);
} else if (this.props.tabLinkChoices && this.props.tabLinkChoices.length == 1) {
// We have only one tab link choice, select it immediately
this.props.tabLinkSelected(this.props.tabLinkChoices[0]);
return (
<div className={styles.configureTab}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<span className={styles.title}>{strings.OneSelectHeading}</span>
<div className={styles.selected + ' ' + styles.tabChoice}>
<span className = {styles.tabName}>{this.props.tabLinkChoices[0].tabName}</span>
<br />
<span className={styles.tabLink}>
<a href={escape(this.props.tabLinkChoices[0].contentPageUrl)}>{escape(this.props.tabLinkChoices[0].contentPageUrl)}</a>
</span>
</div>
</div>
<p className={styles.subTitle}>{escape(this.props.message)}</p>
</div>
</div>
</div>
);
} else {
// No tab link choices available; show message only
return (
<div className={styles.configureTab}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<p className={styles.subTitle}>{escape(this.props.message)}</p>
</div>
</div>
</div>
</div>
);
}
}
}

View File

@ -0,0 +1,34 @@
import * as React from 'react';
import styles from './ConfigureTab.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';
import { ITabLink } from '../model/ITabLink';
import * as strings from 'ConfigureTabWebPartStrings';
export interface IRedirectTabProps {
tabLinkChoices?: ITabLink[];
entityId: string;
}
export class RedirectTab extends React.Component<IRedirectTabProps, {}> {
public render(): React.ReactElement<IRedirectTabProps> {
this.props.tabLinkChoices.forEach((item => {
if (item.entityId.toLowerCase() === this.props.entityId.toLowerCase()) {
window.location.replace(item.contentPageUrl);
}
}));
return (
<div className={styles.configureTab}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<p className={styles.subTitle}>{strings.TabNotDefinedMessage}</p>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,26 @@
import * as React from 'react';
import styles from './ConfigureTab.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';
import { ITabLink } from '../model/ITabLink';
export interface ITabErrorProps {
message: string;
}
export class TabError extends React.Component<ITabErrorProps, {}> {
public render(): React.ReactElement<ITabErrorProps> {
return (
<div className={styles.configureTab}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<p className={styles.subTitle}>{escape(this.props.message)}</p>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
define([], function() {
return {
"PropertyPaneDescription": "Tab configuration properties",
"BasicGroupName": "Configuration",
"TabNamesFieldLabel": "Tab names (one per line)",
"EntityIdsFieldLabel": "Entity Ids (one per line)",
"ContentPageUrlsFieldLabel": "Content page URLs (one per line)",
"TabInstructions": "For each tab, choose a name, an entity Id, and the target page URL. Entity IDs should be unique, URL-safe names for each tab that won't change over time",
"RedirectFieldLabel": "Redirect tabs through this page",
"RedirectFieldInstructions": "Set the tab to redirect through this page so links can be updated centrally",
"BlankTabsErrorMessage": "You must configure at least one tab complete with name, entity ID, and URL",
"UnevenTabsErrorMessage": "You must have the same number of tab names, entity Ids, and page Urls",
"PleaseSelectHeading": "Please select the tab you'd like to display",
"OneSelectHeading": "This tab will be added to your display",
"TabNotDefinedMessage": "Tab is no longer defined, please update the tab settings or contact the application owner"
}
});

View File

@ -0,0 +1,20 @@
declare interface IConfigureTabWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
TabNamesFieldLabel: string;
EntityIdsFieldLabel: string;
ContentPageUrlsFieldLabel: string;
TabInstructions: string;
RedirectFieldLabel: string;
RedirectFieldInstructions: string;
BlankTabsErrorMessage: string;
UnevenTabsErrorMessage: string;
PleaseSelectHeading: string;
OneSelectHeading: string;
TabNotDefinedMessage: string;
}
declare module 'ConfigureTabWebPartStrings' {
const strings: IConfigureTabWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,5 @@
export interface ITabLink {
tabName: string;
entityId: string;
contentPageUrl: string;
}

View File

@ -0,0 +1,35 @@
import * as strings from 'ConfigureTabWebPartStrings';
import { ITabLink } from '../model/ITabLink';
export default class TabLinkParser {
public parseTabLinks(tabNames: string, entityIds: string, contentPageUrls: string): ITabLink[] {
if (!tabNames || !entityIds || !contentPageUrls) {
throw new Error(strings.BlankTabsErrorMessage);
}
var tabNameArray = tabNames.trim().split('\n');
var entityIdArray = entityIds.trim().split('\n');
var contentPageUrlArray = contentPageUrls.trim().split('\n');
var result: ITabLink[] = [];
var length = tabNameArray.length;
if (entityIdArray.length != length || contentPageUrlArray.length != length) {
throw new Error(strings.UnevenTabsErrorMessage);
}
for (let i = 0; i < length; i++) {
if (!tabNameArray[i] || !entityIdArray[i] || !contentPageUrlArray[i]) {
throw new Error(strings.BlankTabsErrorMessage);
}
result.push({
tabName: tabNameArray[i],
entityId: entityIdArray[i],
contentPageUrl: contentPageUrlArray[i]
});
}
return result;
}
}

View File

@ -0,0 +1,33 @@
import * as microsoftTeams from "@microsoft/teams-js";
import { ITabLink } from '../model/ITabLink';
export default class TeamsConfigurationService {
constructor() {
if (microsoftTeams) {
microsoftTeams.initialize();
}
}
public configureTab(tab: ITabLink, redirect: boolean) {
if (microsoftTeams) {
let url = redirect ?
window.location.href + "?entityId=" + tab.entityId :
tab.contentPageUrl;
microsoftTeams.settings.setValidityState(true);
microsoftTeams.settings.registerOnSaveHandler((saveEvent) => {
microsoftTeams.settings.setSettings({
suggestedDisplayName: tab.tabName,
entityId: tab.entityId,
contentUrl: url,
websiteUrl: url
});
saveEvent.notifySuccess();
});
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -1,7 +1,6 @@
import * as React from 'react';
import styles from './FieldVisits.module.scss';
import { ContentType } from '../model/IConversation';
import { IConversationService } from '../services/ConversationService/IConversationService';
import { IMapService } from '../services/MapService/IMapService';
@ -93,7 +92,7 @@ export class PostToChannel extends React.Component<IPostToChannelProps, IPostToC
;
this.props.conversationService
.createChatThread(message, ContentType.html)
.createChatThread(message, "html")
.then(() => {
this.setState({ value: '' });
});

View File

@ -1,19 +0,0 @@
// For use with this API:
// https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/channel_post_chatthreads
// TODO: Replace with something more official
export const enum ContentType {
text = 0,
html = 1
}
export interface IChatMessage {
body: {
contentType: ContentType;
content: string;
};
}
export interface INewChatThread {
rootMessage: IChatMessage;
}

View File

@ -1,9 +1,8 @@
import { IConversationService } from './IConversationService';
import { ContentType } from '../../model/IConversation';
export default class ConversationServiceMock implements IConversationService {
public createChatThread(content: string, contentType: ContentType) {
public createChatThread(content: string, contentType: string) {
console.log(`New chat thread: ${content}`);
return new Promise<void>((resolve) => {

View File

@ -1,4 +1,3 @@
import { INewChatThread, ContentType } from '../../model/IConversation';
import { IConversationService } from '../../services/ConversationService/IConversationService';
import { WebPartContext } from '@microsoft/sp-webpart-base';
@ -18,24 +17,23 @@ export default class ConversationServiceTeams implements IConversationService {
this.channelId = channelId;
}
public createChatThread(content: string, contentType: ContentType) {
public createChatThread(content: string, contentType: string) {
const result = new Promise<void>((resolve, reject) => {
const postContent: INewChatThread =
const postContent =
{
rootMessage: {
body: {
content: content,
contentType: contentType
}
}
};
this.context.msGraphClientFactory
.getClient()
.then((graphClient: MSGraphClient): void => {
graphClient.api(`https://graph.microsoft.com/beta/teams/${this.teamId}/channels/${this.channelId}/chatThreads`)
graphClient.api(
`https://graph.microsoft.com/beta/teams/${this.teamId}/channels/${this.channelId}/messages`)
.post(postContent, ((err, res) => {
resolve();
}));

View File

@ -1,8 +1,6 @@
import { ContentType } from '../../model/IConversation';
export interface IConversationService {
createChatThread(content: string, contentType: ContentType):
createChatThread(content: string, contentType: string):
Promise<void>;
}