Storing MS Teams Personal App Web Part's properties in user's OneDrive

This commit is contained in:
AJIXuMuK 2020-04-24 17:36:29 -07:00
parent 233f77062b
commit 71e404e055
31 changed files with 18143 additions and 0 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,44 @@
## React Teams Personal App Settings Web Part
Sample web part that demostrates how you can store Teams Personal App Web Part's properties in user's OneDrive.
## 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
![drop](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.**
## 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

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,38 @@
{
"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
}
}