Merge pull request #1243 from AJIXuMuK/teams-personal-app-settings
Teams personal app settings
This commit is contained in:
commit
248cea5b72
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export const WebPartKey = 'PnPWebPartSamples';
|
|
@ -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 };
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface IWebPartProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
14
samples/react-teams-personal-app-settings/src/webparts/personalAppSettings/loc/en-us.js
vendored
Normal file
14
samples/react-teams-personal-app-settings/src/webparts/personalAppSettings/loc/en-us.js
vendored
Normal 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"
|
||||
}
|
||||
});
|
17
samples/react-teams-personal-app-settings/src/webparts/personalAppSettings/loc/mystrings.d.ts
vendored
Normal file
17
samples/react-teams-personal-app-settings/src/webparts/personalAppSettings/loc/mystrings.d.ts
vendored
Normal 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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue