Merge pull request #3 from pnp/master

This commit is contained in:
João Mendes 2020-08-17 22:49:08 +01:00 committed by GitHub
commit 2e6547018a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
201 changed files with 80857 additions and 218 deletions

View File

@ -52,6 +52,7 @@ Version|Date|Comments
1.2|June 04, 2020|Added full-width support
1.3|July 07, 2020|Simplified web part
1.4|July 28, 2020|Update styles to minimise toolbar overlap
1.5|July 30, 2020|Update styles to improve full-width mode
## Disclaimer

View File

@ -3,7 +3,7 @@
"solution": {
"name": "workbench-customizer-client-side-solution",
"id": "5d6f4a5a-9d2b-4a93-a283-16b8f5ea75d6",
"version": "1.4.0.0",
"version": "1.5.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},

View File

@ -1,6 +1,6 @@
{
"name": "workbench-customizer",
"version": "1.4.0",
"version": "1.5.0",
"private": true,
"main": "lib/index.js",
"engines": {

View File

@ -15,7 +15,7 @@
.CanvasZone {
margin: auto;
padding-left: 24px;
padding-left: 15px;
}
.CanvasZone--noMargin {

View File

@ -2,7 +2,7 @@
.CanvasComponent {
.CanvasZone {
max-width: 100%;
padding-left: 0;
padding-left: 0 !important;
padding-right: 0;
.CanvasSection {

View File

@ -4902,9 +4902,9 @@
"dev": true
},
"elliptic": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
"integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",

View File

@ -0,0 +1,89 @@
# SPFx Avatar
## Summary
This is a sample web part that helps user create their avatar and save as profile picture. User can even download their avatar as PNG file. This webpart can be useful in Intranet to help user create their avatar and save it as profile picture.
##
![directory](/samples/react-avatar/assets/reactAvatarOutcome.gif)
## Features
* Avatar Generator from multiple options available
* Set Avatar as Profile Picture.
* Download Avatar as PNG file.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Online](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
## Prerequisites
* SharePoint Online tenant
* You have to provide permission in SharePoint admin for accessing Graph API on behalf of your solution. We can do it before deployment as proactive steps, or after deployment. You can refer to [steps about how to do this post-deployment](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aad-tutorial#deploy-the-solution-and-grant-permissions). You have to use API Access Page of SharePoint admin and add below permission for our use case.
```
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite"
}
]
```
## Concepts
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Using react framework in SPFx webpart
* Calling Graph API my Photo to store the image in SPFx webpart
* Usage of Material UI Tab and Dialog
* Usage of [avataaars](https://getavataaars.com/)
## WebPart Properties
Property |Type|Required| comments
--------------------|----|--------|----------
Web Part Title | Text| no|
## Solution
The web part Use avataaars library for creating avatars and MS Graph with User.ReadWrite and User.ReadWriteAll for saving avatar as current users Profile Picture.FileSaver for downloading avatar image as png file.
Solution|Author(s)
--------|---------
react Avatar|Kunj Sangani
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|August 1, 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 repository
- in the command line run:
- `npm install`
- `gulp build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- `Add to AppCatalog and deploy`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-avatar" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 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": {
"avatar-generator-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/avatarGenerator/AvatarGeneratorWebPart.js",
"manifest": "./src/webparts/avatarGenerator/AvatarGeneratorWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"AvatarGeneratorWebPartStrings": "lib/webparts/avatarGenerator/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-avatars",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-avatars-client-side-solution",
"id": "b96dfed7-ec48-4082-ba50-f6c7b09143c7",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite"
}
]
},
"paths": {
"zippedPackage": "solution/react-avatars.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 -->"
}

7
samples/react-avatar/gulpfile.js vendored Normal file
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'));

17235
samples/react-avatar/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "react-avatars",
"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": {
"@material-ui/core": "^4.11.0",
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"avataaars": "^1.2.1",
"file-saver": "^2.0.2",
"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.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": "4052b4ec-72e9-4248-b411-18a4cd438d4b",
"alias": "AvatarGeneratorWebPart",
"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": "AvatarGenerator" },
"description": { "default": "AvatarGenerator for SharePoint Profile Picture" },
"officeFabricIconFontName": "ContactInfo",
"properties": {
"description": "AvatarGenerator"
}
}]
}

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'AvatarGeneratorWebPartStrings';
import AvatarGenerator from './components/AvatarGenerator';
import { IAvatarGeneratorProps } from './components/IAvatarGeneratorProps';
export interface IAvatarGeneratorWebPartProps {
description: string;
}
export default class AvatarGeneratorWebPart extends BaseClientSideWebPart <IAvatarGeneratorWebPartProps> {
public render(): void {
const element: React.ReactElement<IAvatarGeneratorProps> = React.createElement(
AvatarGenerator,
{
description: this.properties.description,
context:this.context
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: 'Web Part Title'
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,93 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.avatarGenerator {
.container {
max-width: 1200px;
margin: 0px auto;
}
.row {
@include ms-Grid-row;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
}
.column4 {
@include ms-Grid-col;
@include ms-lg4;
@include ms-xl4;
}
.column8 {
@include ms-Grid-col;
@include ms-lg8;
@include ms-xl8;
}
.piece{
display: inline-block;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
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;
}
}
}
:global{
#workbenchPageContent{
max-width: 1200px;
}
.ms-ChoiceField{
margin-left: 40px;
}
}

View File

@ -0,0 +1,422 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import styles from './AvatarGenerator.module.scss';
import { IAvatarGeneratorProps } from './IAvatarGeneratorProps';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import { Avatar, Piece, allOptions, OptionContext, AvatarStyle } from "avataaars";
import { TabPanel } from "./TabPanel";
import * as ReactDOM from 'react-dom';
import Button from '@material-ui/core/Button';
import * as FileSaver from 'file-saver';
import { MSGraphClient } from "@microsoft/sp-http";
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
const options: IChoiceGroupOption[] = [
{ key: AvatarStyle.Circle, text: 'Circle' },
{ key: AvatarStyle.Transparent, text: 'Transparent' }
];
export interface IAvatarGeneratorState {
avatarStyle: AvatarStyle;
value: number;
open: boolean;
savedMessage: string;
savedMessageTitle: string;
}
export default class AvatarGenerator extends React.Component<IAvatarGeneratorProps, IAvatarGeneratorState> {
public static childContextTypes = {
optionContext: PropTypes.instanceOf(OptionContext)
};
private avatarRef: Avatar | null = null;
private canvasRef: HTMLCanvasElement | null = null;
private optionContext: OptionContext = new OptionContext(allOptions);
constructor(props: IAvatarGeneratorProps) {
super(props);
this.state = {
avatarStyle: AvatarStyle.Circle,
value: 0,
open: false,
savedMessage: "",
savedMessageTitle: ""
};
}
public getChildContext() {
return { optionContext: this.optionContext };
}
public componentDidMount() {
const { optionContext } = this;
const value = optionContext.options;
optionContext.options.map((option, index) => {
const optionState = optionContext.getOptionState(option.key)!;
if (optionState.available <= 0) {
return null;
}
});
this.forceUpdate();
}
public a11yProps(index) {
return {
id: `scrollable-auto-tab-${index}`,
'aria-controls': `scrollable-auto-tabpanel-${index}`,
};
}
private triggerDownload = (imageBlob: Blob, fileName: string) => {
FileSaver.saveAs(imageBlob, fileName);
}
private saveAsProfile = (imageBlob: Blob) => {
var reader = new FileReader();
var tempfile = new File([imageBlob], "myavataaars.png", { type: "image/png" });
this.props.context.msGraphClientFactory
.getClient().then((client: MSGraphClient) => {
client
.api("me/photo/$value")
.version("v1.0").header("Content-Type", "image/png").put(tempfile, (err, res) => {
if (!err) {
this.setState({
open: true, savedMessage: "Your profile picture is updated",
savedMessageTitle: "Profile Picture Saved"
});
} else {
this.setState({
open: true, savedMessage: err.message,
savedMessageTitle: "Error while saving Profile Picture"
});
}
});
});
}
private onDownloadPNG = (isDownload: boolean) => {
const svgNode = ReactDOM.findDOMNode(this.avatarRef!);
const canvas = this.canvasRef!;
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const anyWindow = window as any;
const DOMURL = anyWindow.URL || anyWindow.webkitURL || window;
const data = svgNode["outerHTML"];
const img = new Image(canvas.width, canvas.height);
const svg = new Blob([data], { type: 'image/svg+xml' });
const url = DOMURL.createObjectURL(svg);
img.onload = () => {
ctx.save();
ctx.scale(2, 2);
ctx.drawImage(img, 0, 0);
ctx.restore();
DOMURL.revokeObjectURL(url);
this.canvasRef!.toBlob(imageBlob => {
if (isDownload) {
this.triggerDownload(imageBlob!, 'myavataaars.png');
} else {
this.saveAsProfile(imageBlob!);
}
});
};
img.src = url;
}
public _onChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
this.setState({ avatarStyle: option.key as AvatarStyle });
}
public handleClose = () => {
this.setState({ open: false });
}
public render(): React.ReactElement<IAvatarGeneratorProps> {
let count: number = -1;
let internalcount: number = -1;
return (
<div className={styles.avatarGenerator}>
{this.props.description &&
<h1>{this.props.description}</h1>}
<div className={styles.container}>
<div className={styles.row} style={{ textAlign: "center" }}>
<div className={styles.column4}>
<Avatar ref={(ref) => { this.avatarRef = ref; }}
avatarStyle={this.state.avatarStyle}
/>
<ChoiceGroup styles={{ flexContainer: { display: "flex" } }}
defaultSelectedKey={AvatarStyle.Circle} options={options} onChange={this._onChange} label="Avatar Style" />
<Button variant="contained" style={{ marginTop: 10 }} color="primary" onClick={(ev) => {
this.onDownloadPNG(false);
}}>
Save as Profile Picture
</Button>
<Button variant="contained" style={{ marginTop: 10 }} color="primary" onClick={(ev) => {
this.onDownloadPNG(true);
}}>
Download as Image
</Button>
</div>
<div className={styles.column8}>
<AppBar position="static">
<Tabs
variant="scrollable"
scrollButtons="on"
indicatorColor="primary"
value={this.state.value}
onChange={(ev, num) => { this.setState({ value: num }); }}
aria-label="simple tabs example">
{this.optionContext.options.map((option, index) => {
const optionState = this.optionContext.getOptionState(option.key)!;
if (optionState.available <= 0) {
return null;
} else {
count++;
return <Tab label={option.label} {...this.a11yProps(count)}></Tab>;
}
})}
</Tabs>
</AppBar>
{this.optionContext.options.map((option, index) => {
const optionState = this.optionContext.getOptionState(option.key)!;
if (optionState.available <= 0) {
return null;
} else {
internalcount++;
switch (option.key) {
case "topType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle="Circle"
pieceType="top"
pieceSize="100"
topType={type} /></div>);
})}
</TabPanel>;
case "accessoriesType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="accessories"
pieceSize="100"
accessoriesType={type} /></div>);
})}
</TabPanel>;
case "hairColor":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
> <Piece
avatarStyle=""
pieceType="top"
pieceSize="100"
topType="LongHairFro"
hairColor={type} />
</div>);
})}
</TabPanel>;
break;
case "facialHairType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="facialHair"
pieceSize="100"
facialHairType={type} /></div>);
})}
</TabPanel>;
break;
case "clotheType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="clothe"
pieceSize="100"
clotheType={type} /></div>);
})}
</TabPanel>;
break;
case "clotheColor":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="clotheColor"
pieceSize="100"
clotheColor={type} /></div>);
})}
</TabPanel>;
break;
case "graphicType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="graphics"
pieceSize="100"
graphicType={type} /></div>);
})}
</TabPanel>;
break;
case "eyeType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="eyes"
pieceSize="100"
eyeType={type} /></div>);
})}
</TabPanel>;
break;
case "eyebrowType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="eyebrows"
pieceSize="100"
eyebrowType={type} /></div>);
})}
</TabPanel>;
break;
case "mouthType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="mouth"
pieceSize="100"
mouthType={type} /></div>);
})}
</TabPanel>;
break;
case "skinColor":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="skin"
pieceSize="100"
skinColor={type} /></div>);
})}
</TabPanel>;
break;
default:
return null;
break;
}
}
})}
</div>
</div>
<canvas
style={{ display: "none" }}
width='528'
height='560'
ref={(ref) => { this.canvasRef = ref; }}
/>
</div>
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{this.state.savedMessageTitle}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{this.state.savedMessage}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

View File

@ -0,0 +1,6 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IAvatarGeneratorProps {
description: string;
context:WebPartContext;
}

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import Box from '@material-ui/core/Box';
export interface ITabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
export const TabPanel: React.FunctionComponent<ITabPanelProps> = (props: ITabPanelProps) => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`scrollable-auto-tabpanel-${index}`}
aria-labelledby={`scrollable-auto-tab-${index}`}
{...other}
>
{value === index && (
<Box p={3}>
<div>{children}</div>
</Box>
)}
</div>
);
};

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IAvatarGeneratorWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'AvatarGeneratorWebPartStrings' {
const strings: IAvatarGeneratorWebPartStrings;
export = strings;
}

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
}
}

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-dynamic-365",
"libraryId": "5bc3855d-9c59-4322-95e0-78e17dcd809e",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,65 @@
# React Dynamics CRM API
## Summary
This sample shows how to consume Dynamics CRM API using AadTokenProvider class.
![react-dynamics365-api](./assets/screenshot.gif)
## Used SharePoint Framework Version
![SPFx v1.10.0](https://img.shields.io/badge/SPFx-1.10.0-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)
--------|---------
react-dynamics365-api|Ramin Ahmadi
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|Jul 12, 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 repository
* in the command line run:
* `npm install`
* `gulp serve`
### Configuration
* Login to Azure Portal.
* Go to **Azure Active Directory**
* Go to the **App Registrations** page.
* Select **SharePoint Online Client Extensibility** from the list of applications.
* Select **API Permissions**.
* Add **Dynamics CRM** permission.
* Go to the **Manifest** page, and make sure the value for the `allowPublicClient` and the `oauth2AllowImplicitFlow` are both set to `true`.
## Features
This sample illustrates the following concepts on top of the SharePoint Framework:
* Using AadTokenProvider to consume Dynamics CRM API.
* How to get Accounts/Contacts information from Dynamics 365.
* React Hooks
* Using async / await for the async calls
* Ant design for the UI.
> **NOTE:** This sample will not work in the local workbench.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-dynamics-crm-api" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 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": {
"dynamic-365-sales-accounts-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/dynamic365SalesAccounts/Dynamic365SalesAccountsWebPart.js",
"manifest": "./src/webparts/dynamic365SalesAccounts/Dynamic365SalesAccountsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"Dynamic365SalesAccountsWebPartStrings": "lib/webparts/dynamic365SalesAccounts/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-dynamic-365",
"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-dynamic-365-client-side-solution",
"id": "5bc3855d-9c59-4322-95e0-78e17dcd809e",
"version": "1.0.0.1",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Windows Azure Active Directory",
"scope": "User.Read"
}
]
},
"paths": {
"zippedPackage": "solution/react-dynamic-365.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,44 @@
{
"name": "react-dynamic-365",
"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.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"antd": "^4.4.2",
"axios": "^0.19.2",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.7": "^0.6.1",
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

View File

@ -0,0 +1,27 @@
type Listener<T> = (val: T) => void;
type Unsubscriber = () => void;
export class Observable<T> {
private _listeners: Listener<T>[] = [];
constructor(private _val: T) {
}
public get(): T {
return this._val;
}
public set(val: T) {
if (this._val !== val) {
this._val = val;
this._listeners.forEach(l => l(val));
}
}
public subscribe(listener: Listener<T>): Unsubscriber {
this._listeners.push(listener);
return () => {
this._listeners = this._listeners.filter(l => l !== listener);
};
}
}

View File

@ -0,0 +1,12 @@
import { useEffect, useState } from "react";
import { Observable } from "./observable";
export default function useObservable<T>(observable: Observable<T>): T {
const [val, setVal] = useState(observable.get());
useEffect(() => {
return observable.subscribe(setVal);
}, [observable]);
return val;
}

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,5 @@
export default interface IAccount{
name:string;
emailaddress1:string;
accountid:string;
}

View File

@ -0,0 +1,6 @@
export default interface IContact{
fullname : string;
emailaddress1:string;
telephone1:string;
address1_city:string;
}

View File

@ -0,0 +1,60 @@
import axios from "axios";
import { Observable } from "../hooks/observable";
import { AadTokenProviderFactory } from "@microsoft/sp-http";
export default class DynamicsService {
public readonly errorMessage = new Observable<string>(null);
private accessToken: string;
public aadTokenProviderFactory: AadTokenProviderFactory;
// update ResourceURI based on your dynamic crm uri
public resourceUri:string;
public async getAccessToken(){
const token = sessionStorage.getItem("dynamic365Token");
if(token)
this.accessToken = token;
else{
await
this.aadTokenProviderFactory
.getTokenProvider()
.then((tokenProvider) => {
tokenProvider
.getToken(this.resourceUri)
.then((t) => {
this.accessToken = t;
sessionStorage.setItem("dynamic365Token",t);
})
.catch((err) => console.log("Error: " + err));
});
}
}
public async getAccounts() {
const url = `${this.resourceUri}/api/data/v9.0/accounts?$top=20&$select=name,emailaddress1`;
const response = await axios({
url,
method: "GET",
headers:{"Authorization":`Bearer ${this.accessToken}`}
});
return response.data.value;
}
public async getContacts(id:string){
const url = `${this.resourceUri}/api/data/v9.0/contacts?$top=5&$select=fullname,emailaddress1,telephone1,address1_city&$filter=_accountid_value eq '${id}'`;
const response = await axios({
url,
method: "GET",
headers:{"Authorization":`Bearer ${this.accessToken}`}
});
return response.data.value;
}
public async searchAccounts (name:string){
const url = `${this.resourceUri}/api/data/v9.0/accounts?$top=20&$select=name,emailaddress1&$filter=contains(name,'${name}')`;
const response = await axios({
url,
method: "GET",
headers:{"Authorization":`Bearer ${this.accessToken}`}
});
return response.data.value;
}
}

View File

@ -0,0 +1,2 @@
import DynamicsService from './dynamicsService';
export const dynamicsService = new DynamicsService();

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "2a144942-a8bc-4e51-8f37-6ca58450bc00",
"alias": "Dynamic365SalesAccountsWebPart",
"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","TeamsPersonalApp","TeamsTab"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Dynamic 365 Sales Accounts" },
"description": { "default": "Dynamic 365 Sales Accounts description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "Dynamic 365 Sales Accounts"
}
}]
}

View File

@ -0,0 +1,64 @@
import { dynamicsService } from "./../../services/services";
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import * as strings from "Dynamic365SalesAccountsWebPartStrings";
import Dynamic365SalesAccounts from "./components/Dynamic365SalesAccounts";
import { IDynamic365SalesAccountsProps } from "./components/IDynamic365SalesAccountsProps";
import "antd/dist/antd.css";
export interface IDynamic365SalesAccountsWebPartProps {
dynamicCRMDomain: string;
}
export default class Dynamic365SalesAccountsWebPart extends BaseClientSideWebPart<
IDynamic365SalesAccountsWebPartProps
> {
public async onInit(): Promise<void> {
dynamicsService.aadTokenProviderFactory = this.context.aadTokenProviderFactory;
dynamicsService.resourceUri = `https://${this.properties.dynamicCRMDomain}.dynamics.com`;
}
public render(): void {
const element: React.ReactElement<IDynamic365SalesAccountsProps> = React.createElement(
Dynamic365SalesAccounts
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription,
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("dynamicCRMDomain", {
label: strings.DynamicCrmDomainFieldLabel,
}),
],
},
],
},
],
};
}
}

View File

@ -0,0 +1,74 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.dynamic365SalesAccounts {
.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: $ms-color-themeDark;
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;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
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,57 @@
import * as React from "react";
import styles from "./Dynamic365SalesAccounts.module.scss";
import { dynamicsService } from "../../../services/services";
import IAccount from "../../../model/IAccount";
import Search from "antd/lib/input/Search";
import { Table, Divider } from 'antd';
import Contacts from './contacts';
const Dynamic365SalesAccounts = () => {
const [accounts, setAccounts] = React.useState<IAccount[]>([]);
const [selectedRowKeys,setSelectedRowKeys] = React.useState([]);
const [selectedRows,setSelectedRows] = React.useState<IAccount[]>([]);
const columns = [
{
title: 'Name',
dataIndex: 'name'
},
{
title: 'Email',
dataIndex: 'emailaddress1'
}
];
React.useEffect(() => {
dynamicsService.getAccessToken().then(_=>{
dynamicsService.getAccounts().then((accs) => setAccounts(accs.map(a=>({...a,key:a.accountid}))));
});
}, []);
const searchAccounts = async (value) =>{
const result: IAccount[] = await dynamicsService.searchAccounts(value);
const accs = result.map(acc=> ({...acc,key:acc.accountid}));
setAccounts(accs);
};
const rowSelection = {
selectedRowKeys,
onChange: (_selectedRowKeys, _selectedRows) => {
setSelectedRowKeys(_selectedRowKeys);
setSelectedRows(_selectedRows);
}
};
return (
<div className={styles.dynamic365SalesAccounts}>
<Search placeholder="Search accounts" onSearch={value => searchAccounts(value)} enterButton />
<Divider/>
<Table
columns={columns}
expandable={{expandedRowRender: record=> <Contacts Id={record.accountid}/>}}
dataSource={accounts}
/>
</div>
);
};
export default Dynamic365SalesAccounts;

View File

@ -0,0 +1,3 @@
export interface IDynamic365SalesAccountsProps {
description: string;
}

View File

@ -0,0 +1,43 @@
import * as React from "react";
import { Table } from "antd";
import IContact from "../../../model/IContact";
import { dynamicsService } from "../../../services/services";
export interface IContactsProps {
Id: string;
}
const Contacts: React.FC<IContactsProps> = (props) => {
const [data, setData] = React.useState<IContact[]>([]);
React.useEffect(()=>{
dynamicsService.getContacts(props.Id).then(res => setData(res));
},[]);
const columns = [
{
title: "Full Name",
dataIndex: "fullname",
render: (text) => <b>{text}</b>
},
{
title: "City",
dataIndex: "address1_city"
},
{
title: "Email",
dataIndex: "emailaddress1"
},
{
title: "Business Phone",
dataIndex: "telephone1",
render: (text)=><a>{text}</a>
},
];
return (
<>
<Table columns={columns} dataSource={data} pagination={false} />
</>
);
};
export default Contacts;

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Dynamic CRM Settings",
"DynamicCrmDomainFieldLabel": "Dynamic CRM Domain (e.x., contoso.crm11)"
}
});

View File

@ -0,0 +1,10 @@
declare interface IDynamic365SalesAccountsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DynamicCrmDomainFieldLabel: string;
}
declare module 'Dynamic365SalesAccountsWebPartStrings' {
const strings: IDynamic365SalesAccountsWebPartStrings;
export = strings;
}

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.7/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,29 @@
{
"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-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -35,7 +35,7 @@ react-enhanced-powerapps | Hugo Bernier ([Tahoe Ninjas](http://tahoeninjas.blog/
Version|Date|Comments
-------|----|--------
1.0|July 27, 2020|Initial release
1.1|August 15, 2020|Added improved resize event handler
## 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.**

View File

@ -3,7 +3,7 @@
"solution": {
"name": "react-enhanced-powerapps-client-side-solution",
"id": "601b7688-1e9c-468c-b9a3-e4bb4e14edcd",
"version": "1.0.0.0",
"version": "1.1.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"developer": {

View File

@ -36,5 +36,10 @@ export default class EnhancedPowerAppsWebPart extends BaseClientSideWebPart<IEnh
* @param args The new theme
*/
private _handleThemeChangedEvent;
/**
* Redraws the web part when resized
* @param _newWidth
*/
protected onAfterResize(_newWidth: number): void;
}
//# sourceMappingURL=EnhancedPowerAppsWebPart.d.ts.map

View File

@ -1 +1 @@
{"version":3,"file":"EnhancedPowerAppsWebPart.d.ts","sourceRoot":"","sources":["../../../src/webparts/enhancedPowerApps/EnhancedPowerAppsWebPart.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,EACL,0BAA0B,EAK3B,MAAM,6BAA6B,CAAC;AAMrC;;GAEG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D;;GAEG;AACH,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAG3B,MAAM,4BAA4B,CAAC;AAuBpC,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,aAAa,GAAC,aAAa,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAC,KAAK,GAAC,OAAO,GAAC,KAAK,GAAC,QAAQ,CAAC;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,qBAAqB,CAAC,8BAA8B,CAAC;IACzG,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,aAAa,CAA6B;IAElD,SAAS,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAa1B,MAAM,IAAI,IAAI;IAqDrB,SAAS,CAAC,SAAS,IAAI,IAAI;uBAIb,WAAW,EAAI,OAAO;IAIpC,SAAS,CAAC,4BAA4B,IAAI,0BAA0B;uBAwItD,kBAAkB,EAAI,0BAA0B;IAU9D,OAAO,CAAC,YAAY,CAEnB;IAED;;;;KAIC;IACD,OAAO,CAAC,wBAAwB;CAIjC"}
{"version":3,"file":"EnhancedPowerAppsWebPart.d.ts","sourceRoot":"","sources":["../../../src/webparts/enhancedPowerApps/EnhancedPowerAppsWebPart.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,EACL,0BAA0B,EAK3B,MAAM,6BAA6B,CAAC;AAMrC;;GAEG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D;;GAEG;AACH,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAG3B,MAAM,4BAA4B,CAAC;AAuBpC,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,aAAa,GAAC,aAAa,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAC,KAAK,GAAC,OAAO,GAAC,KAAK,GAAC,QAAQ,CAAC;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,qBAAqB,CAAC,8BAA8B,CAAC;IACzG,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,aAAa,CAA6B;IAElD,SAAS,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAa1B,MAAM,IAAI,IAAI;IA4DrB,SAAS,CAAC,SAAS,IAAI,IAAI;uBAIb,WAAW,EAAI,OAAO;IAIpC,SAAS,CAAC,4BAA4B,IAAI,0BAA0B;uBAwItD,kBAAkB,EAAI,0BAA0B;IAU9D,OAAO,CAAC,YAAY,CAEnB;IAED;;;;KAIC;IACD,OAAO,CAAC,wBAAwB;IAKhC;;;OAGG;IACH,SAAS,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAIjD"}

View File

@ -54,9 +54,12 @@ var EnhancedPowerAppsWebPart = /** @class */ (function (_super) {
return _super.prototype.onInit.call(this);
};
EnhancedPowerAppsWebPart.prototype.render = function () {
var clientWidth = this.domElement.clientWidth;
// Context variables and dynamic properties
var dynamicProp = this.properties.dynamicProp.tryGetValue();
var locale = this.context.pageContext.cultureInfo.currentCultureName;
// Get the client width. This is how we'll calculate the aspect ratio and resize the iframe
var clientWidth = this.domElement.clientWidth;
// Get the aspect width and height based on aspect ratio for the web part
var aspectWidth;
var aspectHeight;
switch (this.properties.aspectratio) {
@ -77,9 +80,12 @@ var EnhancedPowerAppsWebPart = /** @class */ (function (_super) {
aspectHeight = 3;
break;
case "Custom":
// Custom aspects just use the width and height properties
aspectWidth = this.properties.width;
aspectHeight = this.properties.height;
}
// If we're using fixed height, we pass the height and don't resize, otherwise we
// calculate the height based on the web part's width and selected aspect ratio
var clientHeight = this.properties.layout === 'FixedHeight' ?
this.properties.height :
clientWidth * (aspectHeight / aspectWidth);
@ -265,6 +271,14 @@ var EnhancedPowerAppsWebPart = /** @class */ (function (_super) {
this._themeVariant = args.theme;
this.render();
};
/**
* Redraws the web part when resized
* @param _newWidth
*/
EnhancedPowerAppsWebPart.prototype.onAfterResize = function (_newWidth) {
// redraw the web part
this.render();
};
return EnhancedPowerAppsWebPart;
}(BaseClientSideWebPart));
export default EnhancedPowerAppsWebPart;

View File

@ -1 +1 @@
{"version":3,"file":"EnhancedPowerApps.d.ts","sourceRoot":"","sources":["../../../../src/webparts/enhancedPowerApps/components/EnhancedPowerApps.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAYpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,KAAK,CAAC,SAAS,CAAC,uBAAuB,EAAE,uBAAuB,CAAC;IAEvG,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,uBAAuB,CAAC;CAkE7D"}
{"version":3,"file":"EnhancedPowerApps.d.ts","sourceRoot":"","sources":["../../../../src/webparts/enhancedPowerApps/components/EnhancedPowerApps.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAYpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,KAAK,CAAC,SAAS,CAAC,uBAAuB,EAAE,uBAAuB,CAAC;IAEvG,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,uBAAuB,CAAC;CAiE7D"}

View File

@ -24,9 +24,11 @@ var EnhancedPowerApps = /** @class */ (function (_super) {
return _super !== null && _super.apply(this, arguments) || this;
}
EnhancedPowerApps.prototype.render = function () {
var _a = this.props, dynamicProp = _a.dynamicProp, themeVariant = _a.themeVariant, themeValues = _a.themeValues, appWebLink = _a.appWebLink, useDynamicProp = _a.useDynamicProp, dynamicPropName = _a.dynamicPropName, locale = _a.locale, border = _a.border, height = _a.height, width = _a.width;
var _a = this.props, dynamicProp = _a.dynamicProp, themeVariant = _a.themeVariant, themeValues = _a.themeValues, appWebLink = _a.appWebLink, useDynamicProp = _a.useDynamicProp, dynamicPropName = _a.dynamicPropName, locale = _a.locale, border = _a.border, height = _a.height;
// The only thing we need for this web part to be configured is an app link or app id
var needConfiguration = !appWebLink;
var semanticColors = themeVariant.semanticColors;
// If we passed a dynamic property, add it as a query string parameter
var dynamicPropValue = useDynamicProp && dynamicProp !== undefined ? "&" + encodeURIComponent(dynamicPropName) + "=" + encodeURIComponent(dynamicProp) : '';
// We can take an app id or a full link. We'll assume (for now) that people are passing a valid app URL
// would LOVE to find an API to retrieve list of valid apps
@ -46,8 +48,7 @@ var EnhancedPowerApps = /** @class */ (function (_super) {
}
// Build the frame url
var frameUrl = appUrl + "?source=SPClient-EnhancedPowerAppsWebPart&amp;locale=" + locale + "&amp;enableOnBehalfOf=true&amp;authMode=onbehalfof&amp;hideNavBar=true&amp;" + dynamicPropValue + themeParams + "&locale=" + locale;
console.log("URL", frameUrl);
return (React.createElement("div", { className: styles.enhancedPowerApps, style: { height: height + "px" } },
return (React.createElement("div", { className: styles.enhancedPowerApps, style: needConfiguration ? { height: "315px" } : { height: height + "px" } },
needConfiguration &&
React.createElement(Placeholder, { iconName: 'PowerApps', iconText: strings.PlaceholderIconText, description: strings.PlaceholderDescription, buttonLabel: strings.PlaceholderButtonLabel, onConfigure: this.props.onConfigure }),
!needConfiguration &&

View File

@ -1 +1 @@
{"version":3,"file":"EnhancedPowerApps.js","sourceRoot":"","sources":["../../../../src/webparts/enhancedPowerApps/components/EnhancedPowerApps.tsx"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,MAAM,MAAM,iCAAiC,CAAC;AAErD,OAAO,KAAK,OAAO,MAAM,iCAAiC,CAAC;AAE3D;;KAEK;AACL,OAAO,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAQvE;IAA+C,qCAAiE;IAAhH;;IAoEA,CAAC;IAlEQ,kCAAM,GAAb;QACQ,IAAA,eAWU,EAVd,4BAAW,EACX,8BAAY,EACZ,4BAAW,EACX,0BAAU,EACV,kCAAc,EACd,oCAAe,EACf,kBAAM,EACN,kBAAM,EACN,kBAAM,EACN,gBACc,CAAC;QACjB,IAAM,iBAAiB,GAAY,CAAC,UAAU,CAAC;QAEvC,IAAA,4CAAc,CAAkC;QAExD,IAAM,gBAAgB,GAAW,cAAc,IAAI,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,MAAI,kBAAkB,CAAC,eAAe,CAAC,SAAI,kBAAkB,CAAC,WAAW,CAAG,CAAA,CAAC,CAAA,EAAE,CAAC;QAE/J,uGAAuG;QACvG,2DAA2D;QAC3D,IAAM,MAAM,GAAW,UAAU,IAAI,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,qCAAmC,UAAY,CAAC,CAAC,CAAC,UAAU,CAAC;QAExI,gEAAgE;QAChE,IAAI,WAAW,GAAW,EAAE,CAAC;QAE7B,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE;YAC3C,WAAW,CAAC,OAAO,CAAC,UAAC,UAAkB;gBACrC,IAAI;oBAEF,IAAM,UAAU,GAAW,cAAc,CAAC,UAAU,CAAC,CAAC;oBACtD,WAAW,GAAG,WAAW,IAAG,MAAI,UAAU,SAAI,kBAAkB,CAAC,UAAU,CAAG,CAAA,CAAC;iBAChF;gBAAC,OAAO,CAAC,EAAE;oBACV,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;iBAChB;YAEH,CAAC,CAAC,CAAC;SACF;QAGD,sBAAsB;QACtB,IAAM,QAAQ,GAAc,MAAM,6DAAwD,MAAM,mFAA8E,gBAAgB,GAAG,WAAW,gBAAW,MAAQ,CAAC;QAEhO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAE7B,OAAO,CACL,6BAAK,SAAS,EAAG,MAAM,CAAC,iBAAiB,EAAG,KAAK,EAAE,EAAC,MAAM,EAAI,MAAM,OAAI,EAAC;YACtE,iBAAiB;gBACjB,oBAAC,WAAW,IACZ,QAAQ,EAAC,WAAW,EACpB,QAAQ,EAAE,OAAO,CAAC,mBAAmB,EACrC,WAAW,EAAE,OAAO,CAAC,sBAAsB,EAC3C,WAAW,EAAE,OAAO,CAAC,sBAAsB,EAC3C,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAI;YACtC,CAAC,iBAAiB;gBACnB,0CACC,IAAI,CAAC,KAAK,CAAC,UAAU;oBACvB,gCAAQ,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAC,IAAI,EAAC,KAAK,EAAC,sDAAsD,EAAC,OAAO,EAAC,gIAAgI,EAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EACnR,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAA,CAAC,CAAC,GAAG,GACpB,CAEN,CAEA,CACP,CAAC;IACJ,CAAC;IACH,wBAAC;AAAD,CAAC,AApED,CAA+C,KAAK,CAAC,SAAS,GAoE7D"}
{"version":3,"file":"EnhancedPowerApps.js","sourceRoot":"","sources":["../../../../src/webparts/enhancedPowerApps/components/EnhancedPowerApps.tsx"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,MAAM,MAAM,iCAAiC,CAAC;AAErD,OAAO,KAAK,OAAO,MAAM,iCAAiC,CAAC;AAE3D;;KAEK;AACL,OAAO,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAQvE;IAA+C,qCAAiE;IAAhH;;IAmEA,CAAC;IAjEQ,kCAAM,GAAb;QACQ,IAAA,eAUU,EATd,4BAAW,EACX,8BAAY,EACZ,4BAAW,EACX,0BAAU,EACV,kCAAc,EACd,oCAAe,EACf,kBAAM,EACN,kBAAM,EACN,kBACc,CAAC;QAEjB,qFAAqF;QACrF,IAAM,iBAAiB,GAAY,CAAC,UAAU,CAAC;QAEvC,IAAA,4CAAc,CAAkC;QAExD,sEAAsE;QACtE,IAAM,gBAAgB,GAAW,cAAc,IAAI,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,MAAI,kBAAkB,CAAC,eAAe,CAAC,SAAI,kBAAkB,CAAC,WAAW,CAAG,CAAA,CAAC,CAAA,EAAE,CAAC;QAE/J,uGAAuG;QACvG,2DAA2D;QAC3D,IAAM,MAAM,GAAW,UAAU,IAAI,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,qCAAmC,UAAY,CAAC,CAAC,CAAC,UAAU,CAAC;QAExI,gEAAgE;QAChE,IAAI,WAAW,GAAW,EAAE,CAAC;QAE7B,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE;YACzC,WAAW,CAAC,OAAO,CAAC,UAAC,UAAkB;gBACrC,IAAI;oBAEF,IAAM,UAAU,GAAW,cAAc,CAAC,UAAU,CAAC,CAAC;oBACtD,WAAW,GAAG,WAAW,IAAG,MAAI,UAAU,SAAI,kBAAkB,CAAC,UAAU,CAAG,CAAA,CAAC;iBAChF;gBAAC,OAAO,CAAC,EAAE;oBACV,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;iBAChB;YACH,CAAC,CAAC,CAAC;SACJ;QAGD,sBAAsB;QACtB,IAAM,QAAQ,GAAc,MAAM,6DAAwD,MAAM,mFAA8E,gBAAgB,GAAG,WAAW,gBAAW,MAAQ,CAAC;QAEhO,OAAO,CACL,6BAAK,SAAS,EAAG,MAAM,CAAC,iBAAiB,EAAG,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAC,MAAM,EAAC,OAAO,EAAC,CAAA,CAAC,CAAA,EAAC,MAAM,EAAI,MAAM,OAAI,EAAC;YAC3G,iBAAiB;gBACjB,oBAAC,WAAW,IACZ,QAAQ,EAAC,WAAW,EACpB,QAAQ,EAAE,OAAO,CAAC,mBAAmB,EACrC,WAAW,EAAE,OAAO,CAAC,sBAAsB,EAC3C,WAAW,EAAE,OAAO,CAAC,sBAAsB,EAC3C,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAI;YACtC,CAAC,iBAAiB;gBACnB,0CACC,IAAI,CAAC,KAAK,CAAC,UAAU;oBACvB,gCAAQ,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAC,IAAI,EAAC,KAAK,EAAC,sDAAsD,EAAC,OAAO,EAAC,gIAAgI,EAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EACnR,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAA,CAAC,CAAC,GAAG,GACpB,CAEN,CAEA,CACP,CAAC;IACJ,CAAC;IACH,wBAAC;AAAD,CAAC,AAnED,CAA+C,KAAK,CAAC,SAAS,GAmE7D"}

View File

@ -1,2 +1 @@
.enhancedPowerApps_e02a5fc2{color:inherit}.enhancedPowerApps_e02a5fc2 span[class^=placeholderText_]{font-size:20px!important;font-weight:600!important;color:#323130}.enhancedPowerApps_e02a5fc2 span[class^=placeholderDescriptionText_]{color:#605e5c!important;margin:20px 0 28px;line-height:23px;font-size:16px!important}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNyY1xcd2VicGFydHNcXGVuaGFuY2VkUG93ZXJBcHBzXFxjb21wb25lbnRzXFxFbmhhbmNlZFBvd2VyQXBwcy5tb2R1bGUuc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSw0QkFDRSxNQUFBLFFBREYsMERBU0ksVUFBQSxlQUNBLFlBQUEsY0FDQSxNQUFBLFFBWEoscUVBZUksTUFBQSxrQkFDQSxPQUFBLEtBQUEsRUFBQSxLQUNBLFlBQUEsS0FDQSxVQUFBIn0= */

View File

@ -1,6 +1,6 @@
{
"name": "react-enhanced-powerapps",
"version": "0.0.1",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2211,6 +2211,15 @@
"statuses": "1"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
"yallist": "^3.0.2"
}
},
"mime": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz",
@ -2343,6 +2352,44 @@
"terser": "^3.16.1",
"webpack-sources": "^1.1.0",
"worker-farm": "^1.5.2"
},
"dependencies": {
"cacache": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz",
"integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.4",
"graceful-fs": "^4.1.15",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.3",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"uuid": {
@ -2381,6 +2428,18 @@
"watchpack": "^1.5.0",
"webpack-sources": "^1.3.0"
}
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
}
}
},
@ -5551,51 +5610,6 @@
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
"dev": true
},
"cacache": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz",
"integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.4",
"graceful-fs": "^4.1.15",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.3",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
},
"dependencies": {
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
"yallist": "^3.0.2"
}
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
}
}
},
"cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "react-enhanced-powerapps",
"version": "0.0.1",
"version": "1.1.0",
"private": true,
"main": "lib/index.js",
"engines": {

View File

@ -306,4 +306,13 @@ export default class EnhancedPowerAppsWebPart extends BaseClientSideWebPart<IEnh
this._themeVariant = args.theme;
this.render();
}
/**
* Redraws the web part when resized
* @param _newWidth
*/
protected onAfterResize(_newWidth: number): void {
// redraw the web part
this.render();
}
}

View File

@ -26,8 +26,7 @@ export default class EnhancedPowerApps extends React.Component<IEnhancedPowerApp
dynamicPropName,
locale,
border,
height,
width
height
} = this.props;
// The only thing we need for this web part to be configured is an app link or app id
@ -61,10 +60,8 @@ export default class EnhancedPowerApps extends React.Component<IEnhancedPowerApp
// Build the frame url
const frameUrl: string = `${appUrl}?source=SPClient-EnhancedPowerAppsWebPart&amp;locale=${locale}&amp;enableOnBehalfOf=true&amp;authMode=onbehalfof&amp;hideNavBar=true&amp;${dynamicPropValue}${themeParams}&locale=${locale}`;
console.log("URL", frameUrl);
return (
<div className={ styles.enhancedPowerApps } style={{height:`${height}px`}}>
<div className={ styles.enhancedPowerApps } style={needConfiguration ? {height:`315px`}:{height:`${height}px`}}>
{needConfiguration &&
<Placeholder
iconName='PowerApps'

View File

@ -686,7 +686,7 @@ var MANIFESTS_ARRAY = [
"id": "3d2f89fa-3b9e-4876-82c2-c15ff82190f1",
"alias": "EnhancedPowerAppsWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"version": "1.1.0",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportsFullBleed": true,
@ -725,11 +725,57 @@ var MANIFESTS_ARRAY = [
},
"EnhancedPowerAppsWebPartStrings": {
"defaultPath": "lib/webparts/enhancedPowerApps/loc/en-us.js",
"type": "localizedPath"
"type": "localizedPath",
"paths": {}
},
"PropertyControlStrings": {
"defaultPath": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"type": "localizedPath"
"type": "localizedPath",
"paths": {
"en-US": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"bn": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"chr": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"dv": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"div": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"en": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"fil": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"haw": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"iu": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"lo": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"moh": "node_modules/@pnp/spfx-property-controls/lib/loc/en-us.js",
"fr-FR": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"gsw": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"br": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"tzm-Tfng": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"co": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"fr": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"ff": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"lb": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"mg": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"oc": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"zgh": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"wo": "node_modules/@pnp/spfx-property-controls/lib/loc/fr-fr.js",
"nl-NL": "node_modules/@pnp/spfx-property-controls/lib/loc/nl-nl.js",
"nl": "node_modules/@pnp/spfx-property-controls/lib/loc/nl-nl.js",
"fy": "node_modules/@pnp/spfx-property-controls/lib/loc/nl-nl.js",
"no": "node_modules/@pnp/spfx-property-controls/lib/loc/no.js",
"ru-RU": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"ru": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"ba": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"be": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"ky": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"mn": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"sah": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"tg": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"tt": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"tk": "node_modules/@pnp/spfx-property-controls/lib/loc/ru-ru.js",
"zh-CN": "node_modules/@pnp/spfx-property-controls/lib/loc/zh-cn.js",
"zh": "node_modules/@pnp/spfx-property-controls/lib/loc/zh-cn.js",
"mn-Mong": "node_modules/@pnp/spfx-property-controls/lib/loc/zh-cn.js",
"bo": "node_modules/@pnp/spfx-property-controls/lib/loc/zh-cn.js",
"ug": "node_modules/@pnp/spfx-property-controls/lib/loc/zh-cn.js",
"ii": "node_modules/@pnp/spfx-property-controls/lib/loc/zh-cn.js"
}
},
"@microsoft/sp-property-pane": {
"type": "component",

File diff suppressed because one or more lines are too long

View File

@ -4715,9 +4715,9 @@
"dev": true
},
"elliptic": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
"integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",

View File

@ -6856,9 +6856,9 @@
"dev": true
},
"elliptic": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
"integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",

View File

@ -1961,8 +1961,8 @@ block-stream@*:
inherits "~2.0.0"
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
version "4.11.9"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
body-parser@1.18.3:
version "1.18.3"
@ -3150,8 +3150,8 @@ electron-to-chromium@^1.3.390:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.393.tgz#d13fa4cbf5065e18451c84465d22aef6aca9a911"
elliptic@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
version "6.5.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
@ -4421,11 +4421,11 @@ hash-base@^3.0.0:
safe-buffer "^5.0.1"
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
dependencies:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
minimalistic-assert "^1.0.1"
hawk@~6.0.2:
version "6.0.2"
@ -4651,17 +4651,17 @@ inherits@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b"
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
inherits@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
ini@^1.3.4, ini@~1.3.0:
version "1.3.5"
@ -6568,7 +6568,7 @@ mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
minimalistic-assert@^1.0.0:
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"

View File

@ -61,11 +61,13 @@ Built with SharePoint Framework GA, Office Graph, React and Chart.JS
Solution|Author(s)
--------|---------
react-modern-charts|Jeremy Coleman (MCP, PC Professional, Inc.)
react-modern-charts|Peter Paul Kirschner ([@petkir_at](https://twitter.com/petkir_at))
## Version history
Version|Date|Comments
-------|----|--------
1.0.0.3|July 30, 2020| Support for Managed Metadata Field(Single) as Label
1.0.0.2|February 09, 2020| Upgrade to SPFx 1.10.0
1.0.0.1|April 25, 2018|Update to SPFx 1.4.1
1.0.0.0|February 11, 2017|Initial release

View File

@ -3,7 +3,7 @@
"solution": {
"name": "modern-charts-client-side-solution",
"id": "f8a78a9a-a93e-4843-89e5-7b871d9b9fa2",
"version": "1.0.0.2",
"version": "1.0.0.3",
"isDomainIsolated": false,
"includeClientSideAssets": true
},

View File

@ -1,45 +0,0 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"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-case": true,
"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,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -20,6 +20,7 @@ export interface ChartConfiguration {
theme: string;
bgColors: Array<string>;
hoverColors: Array<string>;
hasTaxField?: boolean;
}
export interface IModernChartsWebPartProps {

View File

@ -31,6 +31,11 @@ export interface ISPList {
Id: string;
}
export interface IFieldProperty extends IPropertyPaneDropdownOption {
fieldtype: string;
}
export default class ModernChartsWebPart extends BaseClientSideWebPart<IModernChartsWebPartProps> {
private reactCharts: React.ReactElement<IModernChartsProps>;
@ -205,12 +210,29 @@ export default class ModernChartsWebPart extends BaseClientSideWebPart<IModernCh
data.forEach((item) => {
if (chLabels['unique'].indexOf(item[config.unique]) == -1 && item[config.unique] != null && item[config.unique] != "") {
chLabels['unique'].push(item[config.unique]);
chLabels['labels'].push(item[config.col1]);
//if term use VAlue
chLabels['labels'].push(this.getLabel(item, config.col1));
}
});
return chLabels;
}
private getLabel(item: Object, col: string) {
if (!!item[col] && !!item[col]['WssId'] && !!item["TaxCatchAll"] && item["TaxCatchAll"].length > 0) {
//Filter because you can have more Fields from this type to select the right value
const wssid: number = item[col]['WssId'];
const terms = (item["TaxCatchAll"]).filter((x) => x.ID === wssid);
if (!!terms && terms.length > 0) {
return terms[0].Term;
}
return 'TermLabel not Found';
}
//TODO HyperLink
//lookup user
return item[col];
}
private getValues(data: Array<Object>, unique: Array<string>, config: ChartConfiguration): Array<Array<any>> {
const values: Object = {};
@ -270,6 +292,21 @@ export default class ModernChartsWebPart extends BaseClientSideWebPart<IModernCh
this.properties.chartConfig[pPathInd].bgColors = newTheme['bgColors'];
this.properties.chartConfig[pPathInd].hoverColors = newTheme['hoverColors'];
}
if (pPath === 'col1' && (newValue != oldValue)) {
this.properties.chartConfig[pPathInd].hasTaxField = false;
if (!!newValue
&& !!this.properties.chartConfig[pPathInd].columns
&& this.properties.chartConfig[pPathInd].columns.length > 0
) {
const selects = this.properties.chartConfig[pPathInd].columns.filter(f => f.key === newValue);
if (selects.length > 0) {
const selected = selects[0];
this.properties.chartConfig[pPathInd].hasTaxField = selected.fieldtype === 'TaxonomyFieldType';
}
}
}
//col1
this.context.propertyPane.refresh();
this.render();
}
@ -457,7 +494,14 @@ export default class ModernChartsWebPart extends BaseClientSideWebPart<IModernCh
}
public getData(chartConfig: Object) {
return this.context.spHttpClient.get(chartConfig['dataurl'] + `/_api/web/lists/GetByTitle(\'${chartConfig['list']}\')/items?$orderby=Id desc&$limit=10&$top=${this.properties.maxResults}`, SPHttpClient.configurations.v1)
const urlparttax = '&$select=*,TaxCatchAll/Term,TaxCatchAll/ID&$expand=TaxCatchAll';
const resturl = `/_api/web/lists/GetByTitle(\'${chartConfig['list']}\')/items?$orderby=Id desc&$limit=10&$top=${this.properties.maxResults}`;
let requesturl = chartConfig['dataurl'] + resturl;
if (!!chartConfig['hasTaxField']) {
requesturl = requesturl + urlparttax;
}
return this.context.spHttpClient.get(requesturl, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
});
@ -480,10 +524,14 @@ export default class ModernChartsWebPart extends BaseClientSideWebPart<IModernCh
private _updateListColumns(siteUrl: string, listName: string, _chartConfig: ChartConfiguration): void {
this._getListColumns(listName, siteUrl).then((response) => {
var respLists: IPropertyPaneDropdownOption[] = [];
var respLists: IFieldProperty[] = [];
console.log(response.value);
for (var _key in response.value) {
respLists.push({ key: response.value[_key]['InternalName'], text: response.value[_key]['Title'] });
respLists.push({
key: response.value[_key]['InternalName'],
text: response.value[_key]['Title'],
fieldtype: response.value[_key]['TypeAsString']
});
}
this._columnOptions = respLists;
_chartConfig.columns = respLists;

View File

@ -52,19 +52,19 @@ export default class ModernCharts extends React.Component<IModernChartsProps, {}
tChart = <Doughnut data={data} options={options} />;
return tChart;
case 'line':
tChart = <Line data={data} options={options} />;
return tChart;
debugger;
return <Line data={data} options={options} legend={{ display: false }} />;
case 'pie':
tChart = <Pie data={data} options={options} />;
return tChart;
case 'bar':
tChart = <Bar data={data} options={options} />;
tChart = <Bar data={data} options={options} legend={{ display: false }} />;
return tChart;
case 'horizontalbar':
tChart = <HorizontalBar data={data} options={options} />;
tChart = <HorizontalBar data={data} options={options} legend={{ display: false }} />;
return tChart;
case 'radar':
tChart = <Radar data={data} options={options} />;
tChart = <Radar data={data} options={options} legend={{ display: false }} />;
return tChart;
case 'polar':
tChart = <Polar data={data} options={options} />;

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,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
}

View File

@ -0,0 +1,13 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.11.0",
"libraryName": "spfx-msgraph-peoplesearch",
"libraryId": "98a8d9d1-47c4-477c-addd-ecae95b235cc",
"environment": "spo",
"packageManager": "npm",
"framework": "react",
"isCreatingSolution": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,87 @@
# Microsoft Graph People Search Web Part
## Summary
Show and search users from your organization, through Microsoft Graph. Search results show as a nice People Card, and display the Live Persona Card on hover.
The web part accepts a search query through a Dynamic Data connection, to further filter the displayed results. A source for this search query is not provided, but by default this can come from the Microsoft Search search box or the Page Environment. You could also use the Search Box Web Part provided by the [PnP Modern Search Web Parts](https://microsoft-search.github.io/pnp-modern-search/).
![directory](./assets/MicrosoftGraphPeopleSearch.gif)
![directory](./assets/MicrosoftGraphPeopleSearch-LPC.gif)
## Future improvements
- Support loading Profile Pictures
- Support for multiple pages
- Improve $select field with predefined properties of the User object
- Improve field mapping with the selected properties defined in $select
- Toggle Live Person Card
## Accompanying blog post
I wrote a blog post covering more if the inner workings, you can find it at [SPFx People Search web part based on Microsoft Graph](https://blog.yannickreekmans.be/spfx-people-search-web-part-based-on-microsoft-graph/)
## Used SharePoint Framework Version
![1.11.0](https://img.shields.io/badge/version-1.11-green.svg)
## Applies to
* [SharePoint Online](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft Teams](https://products.office.com/en-US/microsoft-teams/group-chat-software) - Untested!!
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
react-msgraph-peoplesearch | Yannick Reekmans ([YannickReekmans](https://twitter.com/YannickReekmans))
## Version history
Version|Date|Comments
-------|----|--------
2.0.0|July 30, 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 repository
- in the command line run:
- `npm install`
- `gulp build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- Add to AppCatalog and deploy
- Assign `User.Read.All` delegated permissions to the **SharePoint Online Client Extensibility Web Application Principal**, easiest way is with [Office 365 CLI](https://pnp.github.io/office365-cli/):
```
o365 login
o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'User.Read.All'
```
---
## Acknowledgements / Inspiration
There are many web parts that aim to do the same thing, but they either use SharePoint Search as data store or they render their results in a completely different way. It's impossible to acknowledge all sources of inspiration to this solution, but I do want to give a shout out to two projects (and their contributors) that were foundational to deliver this solution as quickly as I did:
### React Directory Web Part
The foundation on which I started building my own solution. This web part can be downloaded from the [SharePoint Framework Client-Side Web Part Samples & Tutorial Materials](https://github.com/pnp/sp-dev-fx-webparts/tree/master/samples/react-directory)
#### Thanks to
- João Mendes ([@joaojmendes](https://twitter.com/joaojmendes))
- Peter Paul Kirschner ([@petkir_at](https://twitter.com/petkir_at))
### PnP Modern Search Web Parts
These web parts were an enormous inspiration on code structure and implementation approach. Their codebase is very impressive, and a lot of the code in this web part is a literal copy paste from them. You can find more on the [PnP Modern Search Web Parts](https://microsoft-search.github.io/pnp-modern-search/) page.
#### Thanks to
- Franck Cornu (aequos) - [@FranckCornu](http://www.twitter.com/FranckCornu) - [GitHub Sponsor Page](https://github.com/sponsors/FranckyC)
- Mikael Svenson (Microsoft) - [@mikaelsvenson](http://www.twitter.com/mikaelsvenson)
- Yannick Reekmans - [@yannickreekmans](https://twitter.com/yannickreekmans)
- Albert-Jan Schot - [@appieschot](https://twitter.com/appieschot)
- Tarald Gåsbakk (PuzzlePart) - [@taraldgasbakk](https://twitter.com/Taraldgasbakk)
- Brad Schlintz (Microsoft) - [@bschlintz](https://twitter.com/bschlintz)
- Richard Gigan - [@PooLP](https://twitter.com/PooLP)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-msgraph-peoplesearch" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 MiB

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"peoplesearch-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/peoplesearch/PeopleSearchWebPart.js",
"manifest": "./src/webparts/peoplesearch/PeopleSearchWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"PeopleSearchWebPartStrings": "lib/webparts/peoplesearch/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/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": "spfx-msgraph-peoplesearch",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1 @@
{"preset":"@voitanos/jest-preset-spfx-react16","rootDir":"../src","coverageReporters":["text","json","lcov","text-summary","cobertura"],"reporters":["default","jest-junit"]}

View File

@ -0,0 +1,21 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"developer": {
"name": "Contoso",
"privacyUrl": "https://contoso.com/privacy",
"termsOfUseUrl": "https://contoso.com/terms-of-use",
"websiteUrl": "https://contoso.com/my-app",
"mpnId": "000000"
},
"name": "Microsoft Graph People Search",
"id": "98a8d9d1-47c4-477c-addd-ecae95b235cc",
"version": "2.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/spfx-msgraph-peoplesearch.sppkg"
}
}

Some files were not shown because too many files have changed in this diff Show More