Merge pull request #1243 from AJIXuMuK/teams-personal-app-settings

Teams personal app settings
This commit is contained in:
Hugo Bernier 2020-04-25 00:03:11 -04:00 committed by GitHub
commit 248cea5b72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 18380 additions and 201 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.10.0",
"libraryName": "react-teams-personal-app-settings",
"libraryId": "6ba2b4fd-5ef4-4e32-9ec1-bd2ff043131d",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,79 @@
---
page_type: sample
products:
- office-sp
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
platforms:
- react
createdDate: 04/24/2017 12:00:00 AM
---
## React Teams Personal App Settings Web Part
Sample web part that demonstrates how you can store Teams Personal App Web Part's properties in user's OneDrive.
![Teams Personal App](./assets/teams-personal-app-settings.png)
## Details
Teams Personal Apps, or Personal Tabs don't have settings.
For SPFx it means few things:
* Web Part will never be switched to Edit mode
* Property Pane will never be shown
* `this.properties` value is always undefined
But there are definitely scenarios when we want to be able to configure Personal App and store this configuration somehow.
The provided sample demonstrates how it can be achieved using custom Settings Panel and custom list in user's OneDrive.
`OneDriveListWebPartPropertiesService` can be copied from this sample to your web parts and used to implement the same approach.
Downside of this approach is we need to additionally implement "app uninstalled" event to correctly remove properties from OneDrive list.
## Used SharePoint Framework Version
![1.10.0](https://img.shields.io/badge/drop-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](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)
--------|---------
react-teams-personal-app-settings-client-side-solution|[AJIXuMuK](https://github.com/AJIXuMuK)
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 24, 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
* clone this repo
* move to right folder
* in the command line run:
* `npm install`
* `gulp bundle --ship`
* `gulp package-solution --ship`
* from the `sharepoint/solution` folder, deploy the `.sppkg` file to the App catalog in your tenant
* select deployed package in the App Catalog and click **Sync to Teams** in the Ribbon
* Go to Teams and add **Personal App Settings** personal app
## Features
* Using MS Graph to work with SharePoint lists and list items (create list, create and read list items)
* Using React Hooks for implementing custom components
* Exposing SPFx Web Part as MS Teams Personal App
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-teams-personal-app-settings" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"personal-app-settings-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/personalAppSettings/PersonalAppSettingsWebPart.js",
"manifest": "./src/webparts/personalAppSettings/PersonalAppSettingsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"PersonalAppSettingsWebPartStrings": "lib/webparts/personalAppSettings/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-personal-app-settings",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,21 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-teams-personal-app-settings-client-side-solution",
"id": "6ba2b4fd-5ef4-4e32-9ec1-bd2ff043131d",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [{
"resource": "Microsoft Graph",
"scope": "Sites.Manage.All"
}, {
"resource": "Microsoft Graph",
"scope": "Files.Read"
}]
},
"paths": {
"zippedPackage": "solution/react-teams-personal-app-settings.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 -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
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(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "react-teams-personal-app-settings",
"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": {
"react": "16.8.5",
"react-dom": "16.8.5",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"office-ui-fabric-react": "6.189.2",
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"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": "ff0da361-b9be-4ee4-b295-ac1e76bb25ba",
"alias": "PersonalAppSettingsWebPart",
"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": ["TeamsPersonalApp"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Personal App Settings" },
"description": { "default": "Personal App Settings description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "Personal App Settings"
}
}]
}

View File

@ -0,0 +1,64 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'PersonalAppSettingsWebPartStrings';
import PersonalAppSettings from './components/PersonalAppSettings';
import AppContext from './common/AppContext';
import { IWebPartProps } from './common/IWebPartProps';
import { IWebPartPropertiesService } from './services/webPartPropertiesService/IWebPartPropertiesService';
import { OneDriveListWebPartPropertiesService } from './services/webPartPropertiesService/OneDriveListWebPartPropertiesService';
import { WebPartKey } from './common/Constants';
export default class PersonalAppSettingsWebPart extends BaseClientSideWebPart <IWebPartProps> {
private _props: IWebPartProps | null;
private _webPartPropertiesService: IWebPartPropertiesService<IWebPartProps>;
private _onUpdateProps = async (webPartProps: IWebPartProps): Promise<void> => {
this._props = webPartProps;
try {
await this._webPartPropertiesService.setProperties(WebPartKey, webPartProps);
this.render();
}
catch (err) {
this.renderError(err);
}
}
public async onInit(): Promise<any> {
this.context.statusRenderer.displayLoadingIndicator(this.domElement, strings.Loading);
this._webPartPropertiesService = new OneDriveListWebPartPropertiesService<IWebPartProps>(this.context);
try {
this._props = await this._webPartPropertiesService.getProperties(WebPartKey);
}
catch (err) {
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
this.renderError(err);
}
}
public render(): void {
const element: React.ReactElement = React.createElement(
AppContext.Provider,
{
value: {
webPartProps: this._props,
onUpdateProps: this._onUpdateProps
}
},
React.createElement(PersonalAppSettings)
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
}

View File

@ -0,0 +1,10 @@
import { createContext } from 'react';
import { IWebPartProps } from './IWebPartProps';
export interface IAppContext {
webPartProps: IWebPartProps;
onUpdateProps: (webPartProps: IWebPartProps) => void;
}
const AppContext = createContext<IAppContext>(undefined);
export default AppContext;

View File

@ -0,0 +1 @@
export const WebPartKey = 'PnPWebPartSamples';

View File

@ -0,0 +1,18 @@
export interface IListItem {
id: string;
name?: string;
webUrl?: string;
createdDateTime?: Date;
lastModifiedDateTime?: Date;
createdBy?: {
user: {
displayName: string;
}
};
lastModifiedBy?: {
user: {
displayName: string;
}
};
fields?: { [fieldName: string]: any };
}

View File

@ -0,0 +1,4 @@
export interface IWebPartProps {
title: string;
description: string;
}

View File

@ -0,0 +1,11 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
.personalAppSettings {
max-width: 700px;
margin: 0px auto;
padding: 15px;
.edit {
margin-top: 10px;
}
}

View File

@ -0,0 +1,34 @@
import * as React from 'react';
import styles from './PersonalAppSettings.module.scss';
import AppContext, { IAppContext } from '../common/AppContext';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { SettingsPanel } from './settingsPanel/SettingsPanel';
import * as strings from 'PersonalAppSettingsWebPartStrings';
/**
* Component to render web part props
*/
const PersonalAppSettings: React.FC = () => {
// getting context
const { webPartProps } = React.useContext<IAppContext>(AppContext);
// flag if the edit panel is open
const [isSettingsPanelOpen, setIsSettingsPanelOpen] = React.useState<boolean>(false);
return <div className={styles.personalAppSettings}>
<div>
<TextField readOnly={true} label={strings.WebPartTitle} value={webPartProps ? webPartProps.title : ''}></TextField>
<TextField readOnly={true} label={strings.WebPartDescription} value={webPartProps ? webPartProps.description : ''}></TextField>
</div>
<div className={styles.edit}>
<PrimaryButton text={strings.Edit} onClick={() => { setIsSettingsPanelOpen(true); }}></PrimaryButton>
</div>
{isSettingsPanelOpen &&
<SettingsPanel
onClosePanel={() => { setIsSettingsPanelOpen(false); }}
/>
}
</div>;
};
export default PersonalAppSettings;

View File

@ -0,0 +1,76 @@
import * as React from 'react';
import { Panel } from 'office-ui-fabric-react/lib/Panel';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import AppContext from '../../common/AppContext';
import * as strings from 'PersonalAppSettingsWebPartStrings';
/**
* Component props
*/
export interface ISettingsPanelProps {
/**
* Panel close handler
*/
onClosePanel: () => void;
}
/**
* Component to update web part props
*/
export const SettingsPanel: React.FunctionComponent<ISettingsPanelProps> = (props: ISettingsPanelProps) => {
// getting context
const { webPartProps, onUpdateProps } = React.useContext(AppContext);
// title value
const [title, setTitle] = React.useState<string>(webPartProps ? webPartProps.title : '');
// description value
const [description, setDescription] = React.useState<string>(webPartProps ? webPartProps.description : '');
/**
* save button click handler
*/
const save = () => {
onUpdateProps({
title: title,
description: description
});
props.onClosePanel();
};
/**
* Cancel button click handler
*/
const cancel = () => {
props.onClosePanel();
};
/**
* Renders panel footer content
*/
const onRenderFooter = () => {
return <div>
<PrimaryButton text={strings.Save} onClick={save} />
<DefaultButton text={strings.Cancel} onClick={cancel} />
</div>;
};
return (
<Panel
headerText={strings.WebPartSettings}
isOpen={true}
onRenderFooterContent={onRenderFooter}
>
<TextField
label={strings.WebPartTitle}
value={title || ''}
onChange={(e, v) => { setTitle(v); }}>
</TextField>
<TextField
label={strings.WebPartDescription}
value={description || ''}
onChange={(e, v) => { setDescription(v); }}>
</TextField>
</Panel>
);
};

View File

@ -0,0 +1,14 @@
define([], function() {
return {
"PropertiesListNotCreatedError": "Sorry, but we can't create SharePoint List to store the properties. Please, verify with your administrators that OneDrive is enabled for your organization.",
"PropertiesNotSavedError": "Sorry, but we can't save web part properties at that time. Please, try again later.",
"SiteManagePermissionsNotProvisioned": "Some of permissions needed for the Personal App are still being provisioned. It can take few hours. Please, try again later.",
"WebPartSettings": "Web Part Settings",
"WebPartTitle": "Web Part Title",
"WebPartDescription": "Web Part Description",
"Edit": "Edit",
"Save": "Save",
"Cancel": "Cancel",
"Loading": "Loading properties"
}
});

View File

@ -0,0 +1,17 @@
declare interface IPersonalAppSettingsWebPartStrings {
PropertiesListNotCreatedError: string;
PropertiesNotSavedError: string;
SiteManagePermissionsNotProvisioned: string;
WebPartSettings: string;
WebPartTitle: string;
WebPartDescription: string;
Edit: string;
Save: string;
Cancel: string;
Loading: string;
}
declare module 'PersonalAppSettingsWebPartStrings' {
const strings: IPersonalAppSettingsWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,13 @@
/**
* Interface to work with web part properties
*/
export interface IWebPartPropertiesService<T> {
/**
* Gets web part properties
*/
getProperties: (webPartKey: string) => Promise<T | null>;
/**
* Sets web part properties
*/
setProperties: (webPartKey: string, properties: T) => Promise<void>;
}

View File

@ -0,0 +1,202 @@
import { IWebPartPropertiesService } from './IWebPartPropertiesService';
import { IListItem } from '../../common/IListItem';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import * as strings from 'PersonalAppSettingsWebPartStrings';
const PropertiesColumnName = 'WPProperties';
const WebPartUniqueKeyColumnName = 'WPKey';
const MySiteGraphIdStorageKey = 'MySiteId';
const SettingsListIdStorageKey = 'ettingsListId';
const PropertiesListTitle = 'WPProperties';
/**
* IWebPartPropertiesService implementation to store properties in personal OneDrive
*/
export class OneDriveListWebPartPropertiesService<T> implements IWebPartPropertiesService<T> {
// cached properties
private _properties: T | null;
/**
* @param _context WebPartContext
*/
public constructor(private _context: WebPartContext) {}
/**
* Gets properties for the web part base on unique key (Properties OneDrive list can contain properties of multiple web parts).
* @param webPartKey The key of the web part to get properties for.
*/
public async getProperties(webPartKey: string): Promise<T | null> {
if (!this._properties) {
// if there are no cached properties, we're getting them from the OneDrive
const listItem = await this._getPropertiesListItem(webPartKey, true);
// checking if there are any previously saved properties
if (listItem) {
this._properties = JSON.parse(listItem.fields![PropertiesColumnName]);
}
}
return this._properties;
}
/**
* Sets properties for the web part base on unique key (Properties OneDrive list can contain properties of multiple web parts).
* @param webPartKey The key of the web part to get properties for.
*/
public async setProperties(webPartKey: string, properties: T): Promise<void> {
// getting list id
const listId = await this._getSettingListId();
if (!listId) {
// no list found
throw Error(strings.PropertiesListNotCreatedError);
}
// updating internal cache
this._properties = JSON.parse(JSON.stringify(properties));
// converting properties object to a string
const propertiesStr = JSON.stringify(properties);
// getting graph site id (<tenant,site-id,web-id>) to work with
const graphSiteId = await this._getMySiteGraphId();
// getting graph client
const graphClient = await this._context.msGraphClientFactory.getClient();
// checking if there are previously saved properties
const existingItem = await this._getPropertiesListItem(webPartKey, true);
if (existingItem) {
//
// updaging properties
//
const itemId = existingItem.id;
let fields: any = {};
fields[PropertiesColumnName] = propertiesStr;
const updateItemResponse = await graphClient.api(`/sites/${graphSiteId}/lists/${listId}/items/${itemId}/fields`).version('v1.0').patch(fields);
if (updateItemResponse.error) {
throw new Error(strings.PropertiesNotSavedError);
}
}
else {
//
// saving properties for the first time
//
let fields: any = {};
fields[WebPartUniqueKeyColumnName] = webPartKey;
fields.Title = webPartKey;
fields[PropertiesColumnName] = propertiesStr;
const createItemResponse = await graphClient.api(`/sites/${graphSiteId}/lists/${listId}/items`).version('v1.0').post({
fields: fields
});
if (createItemResponse.error) {
throw new Error(strings.PropertiesNotSavedError);
}
}
}
/**
* Gets list item with previously saved properties
* @param webPartKey web part unique key
* @param expandFields flag to expand fields
*/
private async _getPropertiesListItem(webPartKey: string, expandFields: boolean): Promise<IListItem | null | undefined> {
const listId = await this._getSettingListId();
if (!listId) {
throw Error(strings.PropertiesListNotCreatedError);
}
const graphSiteId = await this._getMySiteGraphId();
const graphClient = await this._context.msGraphClientFactory.getClient();
let expandQuery = '';
if (expandFields) {
expandQuery = `&expand=fields`;
}
const existingItemResponse = await graphClient.api(`/sites/${graphSiteId}/lists/${listId}/items?select=id${expandQuery}`).version('v1.0').get();
if (existingItemResponse.value && existingItemResponse.value.length && expandFields) {
return existingItemResponse.value.filter(v => v.fields[WebPartUniqueKeyColumnName] === webPartKey)[0];
}
return null;
}
/**
* Gets MS Graph site ID for current user's OneDrive site
*/
private async _getMySiteGraphId(): Promise<string> {
// we can cache the ID in the localStorage as it will never change for current user
let graphSiteId = window.localStorage.getItem(MySiteGraphIdStorageKey);
if (!graphSiteId) {
const graphClient = await this._context.msGraphClientFactory.getClient();
const currentDomain = location.hostname;
const oneDriveDomain = `${currentDomain.split('.')[0]}-my.sharepoint.com`;
const sharepointIdsResponse = await graphClient.api('/me/drive/root?$select=sharepointIds').version('v1.0').get();
const sharepointIds = sharepointIdsResponse.sharepointIds;
graphSiteId = `${oneDriveDomain},${sharepointIds.siteId},${sharepointIds.webId}`;
window.localStorage.setItem(MySiteGraphIdStorageKey, graphSiteId);
}
return graphSiteId;
}
/**
* Gets settings list id
*/
private async _getSettingListId(): Promise<string | null> {
// we can cache the ID in the localStorage as it will never change
let listId = window.localStorage.getItem(SettingsListIdStorageKey);
if (!listId) {
const graphSiteId = await this._getMySiteGraphId();
const graphClient = await this._context.msGraphClientFactory.getClient();
const listsResponse = await graphClient.api(`/sites/${graphSiteId}/lists?$filter=displayName eq '${PropertiesListTitle}'`).version('v1.0').get();
if (listsResponse.value && listsResponse.value.length) {
listId = listsResponse.value[0].id;
window.localStorage.setItem(SettingsListIdStorageKey, listId!);
}
else {
// creating the list if it hasn't been created before
try {
const createListResponse = await graphClient.api(`/sites/${graphSiteId}/lists`).version('v1.0').post({
displayName: PropertiesListTitle,
columns: [{
name: WebPartUniqueKeyColumnName,
text: {}
}, {
name: PropertiesColumnName,
text: {
allowMultipleLines: true,
maxLength: 1000000000,
textType: 'plain'
}
}],
list: {
hidden: true,
template: 'genericList'
}
});
listId = createListResponse.id;
window.localStorage.setItem(SettingsListIdStorageKey, listId!);
}
catch (error) {
if (error.statusCode === 403 || error.accessCode === 'accessDenied') {
throw Error(strings.SiteManagePermissionsNotProvisioned);
}
}
}
}
return listId;
}
}

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,39 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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
}
}