Merge commit '8348f1431362272ea3e8c9898ea468ac2cd69d3d' as 'samples/react-msgraph-peoplesearch'
This commit is contained in:
commit
afbcba4fda
|
@ -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,6 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
# Microsoft Graph People Search Web Part
|
||||
|
||||
## Summary
|
||||
|
||||
Show and search users from your organisation, 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
|
||||
![drop](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)
|
||||
|
||||
## 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)
|
Binary file not shown.
After Width: | Height: | Size: 596 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 MiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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": "spfx-msgraph-peoplesearch",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"preset":"@voitanos/jest-preset-spfx-react16","rootDir":"../src","coverageReporters":["text","json","lcov","text-summary","cobertura"],"reporters":["default","jest-junit"]}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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,29 @@
|
|||
'use strict';
|
||||
|
||||
// check if gulp dist was called
|
||||
if (process.argv.indexOf('dist') !== -1) {
|
||||
// add ship options to command call
|
||||
process.argv.push('--ship');
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
const gulpSequence = require('gulp-sequence');
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
// Create clean distrubution package
|
||||
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
|
||||
// Create clean development package
|
||||
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Custom Framework Specific gulp tasks
|
||||
*/
|
||||
|
||||
|
||||
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "spfx-msgraph-peoplesearch",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"preversion": "node ./tools/pre-version.js",
|
||||
"postversion": "gulp dist",
|
||||
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
|
||||
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.11.0",
|
||||
"@microsoft/sp-lodash-subset": "1.11.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
|
||||
"@microsoft/sp-property-pane": "1.11.0",
|
||||
"@microsoft/sp-webpart-base": "1.11.0",
|
||||
"@pnp/spfx-controls-react": "1.13.2",
|
||||
"@pnp/spfx-property-controls": "1.15.0",
|
||||
"@uifabric/styling": "6.50.7",
|
||||
"immutability-helper": "^2.4.0",
|
||||
"office-ui-fabric-react": "6.194.0",
|
||||
"react": "16.7.0",
|
||||
"react-ace": "^5.8.0",
|
||||
"react-dom": "16.7.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^1.13.0",
|
||||
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||
"@microsoft/sp-build-web": "1.11.0",
|
||||
"@microsoft/sp-module-interfaces": "1.11.0",
|
||||
"@microsoft/sp-tslint-rules": "1.11.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.11.0",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/mocha": "2.2.38",
|
||||
"@types/react": "16.8.8",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1",
|
||||
"gulp-sequence": "1.0.0",
|
||||
"typescript": "~3.3.x"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import * as React from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/theme/textmate';
|
||||
|
||||
export interface IDebugViewProps {
|
||||
|
||||
/**
|
||||
* The debug content to display
|
||||
*/
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface IDebugViewState {
|
||||
}
|
||||
|
||||
export class DebugViewComponent extends React.Component<IDebugViewProps, IDebugViewState> {
|
||||
|
||||
public render() {
|
||||
return <Suspense fallback={""}><AceEditor
|
||||
width="100%"
|
||||
mode="json"
|
||||
theme="textmate"
|
||||
enableLiveAutocompletion={ true }
|
||||
showPrintMargin={ false }
|
||||
showGutter= { true }
|
||||
value={ this.props.content }
|
||||
highlightActiveLine={ true }
|
||||
readOnly={ true }
|
||||
editorProps={
|
||||
{
|
||||
$blockScrolling: Infinity,
|
||||
}
|
||||
}
|
||||
name="CodeView"
|
||||
/></Suspense> ;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
import * as React from 'react';
|
||||
import ITemplateContext from '../../models/ITemplateContext';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import styles from './PeopleViewComponent.module.scss';
|
||||
import * as strings from "PeopleSearchWebPartStrings";
|
||||
import { Shimmer, ShimmerElementType as ElemType, ShimmerElementsGroup } from 'office-ui-fabric-react';
|
||||
import { ITheme } from '@uifabric/styling';
|
||||
|
||||
export interface IPeopleShimmerViewProps {
|
||||
templateContext: ITemplateContext;
|
||||
}
|
||||
|
||||
export interface IPeopleShimmerViewState {
|
||||
}
|
||||
|
||||
export class PeopleShimmerViewComponent extends React.Component<IPeopleShimmerViewProps, IPeopleShimmerViewState> {
|
||||
public render() {
|
||||
const ctx = this.props.templateContext;
|
||||
let mainElement: JSX.Element = null;
|
||||
let resultCountElement: JSX.Element = null;
|
||||
let paginationElement: JSX.Element = null;
|
||||
|
||||
if (isEmpty(ctx.items) || isEmpty(ctx.items.value)) {
|
||||
if (ctx.showResultsCount) {
|
||||
const shimmerLineStyle = {
|
||||
width: '20%'
|
||||
};
|
||||
|
||||
resultCountElement = <div className={styles.resultCount}>
|
||||
<span className="shimmer line" style={shimmerLineStyle}></span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (ctx.showPagination) {
|
||||
paginationElement = null;
|
||||
}
|
||||
|
||||
let personaSize: number;
|
||||
switch (parseInt(ctx.personaSize)) {
|
||||
case 11:
|
||||
personaSize = 32;
|
||||
break;
|
||||
case 12:
|
||||
personaSize = 40;
|
||||
break;
|
||||
case 13:
|
||||
personaSize = 48;
|
||||
break;
|
||||
case 14:
|
||||
personaSize = 72;
|
||||
break;
|
||||
case 15:
|
||||
personaSize = 100;
|
||||
break;
|
||||
default:
|
||||
personaSize = 48;
|
||||
break;
|
||||
}
|
||||
|
||||
const personaCards = [];
|
||||
for (let i = 0; i < ctx.pageSize; i++) {
|
||||
personaCards.push(<div className={styles.documentCardItem} key={i}>
|
||||
<div className={styles.personaCard}>
|
||||
{this._getPersonaCardShimmers(personaSize)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
mainElement = <React.Fragment>
|
||||
<div className={styles.defaultCard}>
|
||||
{resultCountElement}
|
||||
<div className={styles.documentCardContainer}>
|
||||
{personaCards}
|
||||
</div>
|
||||
</div>
|
||||
{paginationElement}
|
||||
</React.Fragment>;
|
||||
}
|
||||
else if (!ctx.showBlank) {
|
||||
mainElement = <div className={styles.noResults}>{strings.NoResultMessage}</div>;
|
||||
}
|
||||
|
||||
return <div className={styles.peopleView}>{mainElement}</div>;
|
||||
}
|
||||
|
||||
private _getPersonaCardShimmers(personaSize: number): JSX.Element {
|
||||
|
||||
const shimmerContent = <div
|
||||
style={{
|
||||
display: 'flex' ,
|
||||
marginBottom: 15
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
paddingTop: 8,
|
||||
paddingRight: 16,
|
||||
paddingBottom: 8,
|
||||
width: '100%'
|
||||
}}>
|
||||
|
||||
<Shimmer
|
||||
theme={this.props.templateContext.themeVariant as ITheme}
|
||||
customElementsGroup={
|
||||
<div style={{ display: 'flex', marginTop: 10 }}>
|
||||
<ShimmerElementsGroup
|
||||
theme={this.props.templateContext.themeVariant as ITheme}
|
||||
backgroundColor={this.props.templateContext.themeVariant.semanticColors.bodyBackground}
|
||||
shimmerElements={[{ type: ElemType.circle, height: personaSize}, { type: ElemType.gap, width: 10, height: personaSize }]}
|
||||
/>
|
||||
<ShimmerElementsGroup
|
||||
theme={this.props.templateContext.themeVariant as ITheme}
|
||||
flexWrap={true}
|
||||
backgroundColor={this.props.templateContext.themeVariant.semanticColors.bodyBackground}
|
||||
width="100%"
|
||||
shimmerElements={[
|
||||
{ type: ElemType.line, width: '30%', height: 10, verticalAlign: 'center' },
|
||||
{ type: ElemType.gap, width: '70%', height: personaSize/2 },
|
||||
{ type: ElemType.line, width: '60%', height: 10, verticalAlign: 'top' },
|
||||
{ type: ElemType.gap, width: '40%', height: (personaSize/2) }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
}/>
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
return shimmerContent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
.peopleView {
|
||||
|
||||
.defaultCard {
|
||||
.documentCardContainer {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.documentCardItem {
|
||||
margin-right: 15px;
|
||||
flex: 0 0 345px;
|
||||
|
||||
.personaCard {
|
||||
margin: 10px;
|
||||
|
||||
:global {
|
||||
.ms-Persona-optionalText, .ms-Persona-tertiaryText {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.ms-Persona-primaryText, .ms-Persona-secondaryText, .ms-Persona-optionalText, .ms-Persona-tertiaryText {
|
||||
width: 213px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resultCount {
|
||||
padding-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder_root {
|
||||
|
||||
.shimmer {
|
||||
background: #f6f7f8;
|
||||
background-image: linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 800px 104px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
-webkit-animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-name: placeholderShimmer;
|
||||
-webkit-animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@-webkit-keyframes placeholderShimmer {
|
||||
0% {
|
||||
background-position: -468px 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 468px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.line.shimmer + .line.shimmer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import * as React from 'react';
|
||||
import ITemplateContext from '../../models/ITemplateContext';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import { PersonaCard } from '../PersonaCard/PersonaCard';
|
||||
import styles from './PeopleViewComponent.module.scss';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import * as strings from "PeopleSearchWebPartStrings";
|
||||
|
||||
export interface IPeopleViewProps {
|
||||
templateContext: ITemplateContext;
|
||||
}
|
||||
|
||||
export interface IPeopleViewState {
|
||||
}
|
||||
|
||||
export class PeopleViewComponent extends React.Component<IPeopleViewProps, IPeopleViewState> {
|
||||
public render() {
|
||||
const ctx = this.props.templateContext;
|
||||
let mainElement: JSX.Element = null;
|
||||
let resultCountElement: JSX.Element = null;
|
||||
let paginationElement: JSX.Element = null;
|
||||
|
||||
if (!isEmpty(ctx.items) && !isEmpty(ctx.items.value)) {
|
||||
if (ctx.showResultsCount) {
|
||||
const resultCount = ctx.items["@odata.count"];
|
||||
|
||||
resultCountElement = <div className={styles.resultCount}>
|
||||
<label className="ms-fontWeight-semibold">{Text.format(strings.ResultsCount, resultCount)}</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (ctx.showPagination) {
|
||||
paginationElement = null;
|
||||
}
|
||||
|
||||
const personaCards = [];
|
||||
for (let i = 0; i < ctx.items.value.length; i++) {
|
||||
personaCards.push(<div className={styles.documentCardItem} key={i}>
|
||||
<div className={styles.personaCard}>
|
||||
<PersonaCard serviceScope={ctx.serviceScope} fieldsConfiguration={ctx.peopleFields} item={ctx.items.value[i]} themeVariant={ctx.themeVariant} personaSize={ctx.personaSize} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
mainElement = <React.Fragment>
|
||||
<div className={styles.defaultCard}>
|
||||
{resultCountElement}
|
||||
<div className={styles.documentCardContainer}>
|
||||
{personaCards}
|
||||
</div>
|
||||
</div>
|
||||
{paginationElement}
|
||||
</React.Fragment>;
|
||||
}
|
||||
else if (!ctx.showBlank) {
|
||||
mainElement = <div className={styles.noResults}>{strings.NoResultMessage}</div>;
|
||||
}
|
||||
|
||||
return <div className={styles.peopleView}>{mainElement}</div>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
import { IComponentFieldsConfiguration } from "../../services/TemplateService/TemplateService";
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||
|
||||
export interface IPersonaCardProps {
|
||||
serviceScope: ServiceScope;
|
||||
item?: MicrosoftGraph.User;
|
||||
fieldsConfiguration?: IComponentFieldsConfiguration[];
|
||||
personaSize?: string;
|
||||
themeVariant?: IReadonlyTheme;
|
||||
|
||||
// Individual content properties (i.e web component attributes)
|
||||
text?: string;
|
||||
secondaryText?: string;
|
||||
tertiaryText?: string;
|
||||
optionalText?: string;
|
||||
upn?: string;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IPersonaCardState {
|
||||
isComponentLoaded: boolean;
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import * as React from 'react';
|
||||
import { IPersonaCardProps } from './IPersonaCardProps';
|
||||
import { IPersonaCardState } from './IPersonaCardState';
|
||||
import {
|
||||
Log, Environment, EnvironmentType,
|
||||
} from '@microsoft/sp-core-library';
|
||||
import { SPComponentLoader } from '@microsoft/sp-loader';
|
||||
|
||||
import {
|
||||
Persona,
|
||||
IPersonaSharedProps,
|
||||
} from 'office-ui-fabric-react';
|
||||
import { ITheme } from '@uifabric/styling';
|
||||
import { TemplateService } from '../../services/TemplateService/TemplateService';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
const LIVE_PERSONA_COMPONENT_ID: string = "914330ee-2df2-4f6e-a858-30c23a812408";
|
||||
|
||||
export class PersonaCard extends React.Component<IPersonaCardProps,IPersonaCardState> {
|
||||
private sharedLibrary: any;
|
||||
|
||||
constructor(props: IPersonaCardProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isComponentLoaded: false,
|
||||
};
|
||||
|
||||
this.sharedLibrary = null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @memberof PersonaCard
|
||||
*/
|
||||
public async componentDidMount() {
|
||||
if (Environment.type !== EnvironmentType.Local) {
|
||||
await this._loadSpfxSharedLibrary();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSpfxSharedLibrary() {
|
||||
|
||||
if (!this.state.isComponentLoaded) {
|
||||
|
||||
try {
|
||||
|
||||
this.sharedLibrary = await SPComponentLoader.loadComponentById(LIVE_PERSONA_COMPONENT_ID);
|
||||
|
||||
this.setState({
|
||||
isComponentLoaded: true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
Log.error(`[LivePersona_Component]`, error, this.props.serviceScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private determinePersonaConfig(): IPersonaCardProps {
|
||||
let processedProps: IPersonaCardProps = this.props;
|
||||
|
||||
if (this.props.fieldsConfiguration && this.props.item) {
|
||||
processedProps = TemplateService.processFieldsConfiguration<IPersonaCardProps>(this.props.fieldsConfiguration, this.props.item);
|
||||
}
|
||||
|
||||
return processedProps;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @private
|
||||
* @returns
|
||||
* @memberof PersonaCard
|
||||
*/
|
||||
private _LivePersonaCard() {
|
||||
let processedProps: IPersonaCardProps = this.determinePersonaConfig();
|
||||
|
||||
return React.createElement(
|
||||
this.sharedLibrary.LivePersonaCard,
|
||||
{
|
||||
className: 'livePersonaCard',
|
||||
clientScenario: "PeopleWebPart",
|
||||
disableHover: false,
|
||||
hostAppPersonaInfo: {
|
||||
PersonaType: "User"
|
||||
},
|
||||
serviceScope: this.props.serviceScope,
|
||||
upn: processedProps.upn,
|
||||
onCardOpen: () => {
|
||||
console.log('LivePersonaCard Open');
|
||||
},
|
||||
onCardClose: () => {
|
||||
console.log('LivePersonaCard Close');
|
||||
},
|
||||
},
|
||||
this._PersonaCard(processedProps)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @private
|
||||
* @returns {JSX.Element}
|
||||
* @memberof PersonaCard
|
||||
*/
|
||||
private _PersonaCard(processedProps?: IPersonaCardProps): JSX.Element {
|
||||
|
||||
if (isEmpty(processedProps)) {
|
||||
processedProps = this.determinePersonaConfig();
|
||||
}
|
||||
|
||||
const persona: IPersonaSharedProps = {
|
||||
theme:this.props.themeVariant as ITheme,
|
||||
text: processedProps.text,
|
||||
secondaryText: processedProps.secondaryText,
|
||||
tertiaryText: processedProps.tertiaryText,
|
||||
optionalText: processedProps.optionalText
|
||||
};
|
||||
|
||||
return <Persona {...persona} size={parseInt(this.props.personaSize)} />;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @returns {React.ReactElement<IPersonaCardProps>}
|
||||
* @memberof PersonaCard
|
||||
*/
|
||||
public render(): React.ReactElement<IPersonaCardProps> {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.state.isComponentLoaded
|
||||
? this._LivePersonaCard()
|
||||
: this._PersonaCard()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,18 @@
|
|||
import { PageCollection } from './PageCollection';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { IComponentFieldsConfiguration } from '../services/TemplateService/TemplateService';
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||
|
||||
interface ITemplateContext {
|
||||
items: PageCollection<MicrosoftGraph.User>;
|
||||
showResultsCount: boolean;
|
||||
showBlank: boolean;
|
||||
showPagination: boolean;
|
||||
peopleFields?: IComponentFieldsConfiguration[];
|
||||
themeVariant?: IReadonlyTheme;
|
||||
serviceScope: ServiceScope;
|
||||
[key:string]: any;
|
||||
}
|
||||
|
||||
export default ITemplateContext;
|
|
@ -0,0 +1,7 @@
|
|||
export interface PageCollection<T> {
|
||||
value: T[];
|
||||
"@odata.nextLink"?: string;
|
||||
"@odata.prevLink"?: string;
|
||||
"@odata.count"?: number;
|
||||
[Key: string]: any;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
enum ResultsLayoutOption {
|
||||
People = 5,
|
||||
Debug = 6
|
||||
}
|
||||
|
||||
export default ResultsLayoutOption;
|
|
@ -0,0 +1,12 @@
|
|||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { PageCollection } from '../../models/PageCollection';
|
||||
|
||||
export interface ISearchService {
|
||||
selectParameter: string[];
|
||||
filterParameter: string;
|
||||
orderByParameter: string;
|
||||
searchParameter: string;
|
||||
pageSize: number;
|
||||
searchUsers(): Promise<PageCollection<MicrosoftGraph.User>>;
|
||||
fetchPage(pageLink: string): Promise<PageCollection<MicrosoftGraph.User>>;
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import { ISearchService } from "./ISearchService";
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import * as peopleSearchResults from './MockSearchServiceResults.json';
|
||||
import { PageCollection } from "../../models/PageCollection";
|
||||
|
||||
export class MockSearchService implements ISearchService {
|
||||
private _selectParameter: string[];
|
||||
private _filterParameter: string;
|
||||
private _orderByParameter: string;
|
||||
private _searchParameter: string;
|
||||
private _pageSize: number;
|
||||
|
||||
public get selectParameter(): string[] { return this._selectParameter; }
|
||||
public set selectParameter(value: string[]) { this._selectParameter = value; }
|
||||
|
||||
public get filterParameter(): string { return this._filterParameter; }
|
||||
public set filterParameter(value: string) { this._filterParameter = value; }
|
||||
|
||||
public get orderByParameter(): string { return this._orderByParameter; }
|
||||
public set orderByParameter(value: string) { this._orderByParameter = value; }
|
||||
|
||||
public get searchParameter(): string { return this._searchParameter; }
|
||||
public set searchParameter(value: string) { this._searchParameter = value; }
|
||||
|
||||
public get pageSize(): number { return this._pageSize; }
|
||||
public set pageSize(value: number) { this._pageSize = value; }
|
||||
|
||||
public async searchUsers(): Promise<PageCollection<MicrosoftGraph.User>> {
|
||||
const timeout = Math.floor(Math.random() * (1000)) + 1;
|
||||
|
||||
let resultData: PageCollection<MicrosoftGraph.User> = this.getResultData("1");
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(resultData);
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
public async fetchPage(currentPage: string): Promise<PageCollection<MicrosoftGraph.User>> {
|
||||
const timeout = Math.floor(Math.random() * (1000)) + 1;
|
||||
|
||||
let resultData: PageCollection<MicrosoftGraph.User> = this.getResultData(currentPage);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(resultData);
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
private getResultData(currentPage: string): PageCollection<MicrosoftGraph.User> {
|
||||
let resultData: PageCollection<MicrosoftGraph.User> = {
|
||||
"@odata.count": peopleSearchResults["@odata.count"],
|
||||
value: peopleSearchResults.value as MicrosoftGraph.User[]
|
||||
};
|
||||
let peopleResults = resultData.value;
|
||||
|
||||
//TODO: Implement select
|
||||
//TODO: Implement filter
|
||||
//TODO: Implement orderBy
|
||||
|
||||
//Pagination
|
||||
let totalPages = Math.ceil(resultData["@odata.count"] / this.pageSize);
|
||||
let currentPageNumber = parseInt(currentPage);
|
||||
let currentPageZeroBased = currentPageNumber-1;
|
||||
peopleResults = peopleResults.slice(currentPageZeroBased * this.pageSize, (currentPageZeroBased * this.pageSize) + this.pageSize);
|
||||
|
||||
if (currentPageNumber < totalPages) {
|
||||
resultData["@odata.nextLink"] = (currentPageNumber + 1).toString();
|
||||
}
|
||||
|
||||
if (currentPageNumber > 0) {
|
||||
resultData["@odata.prevLink"] = (currentPageNumber - 1).toString();
|
||||
}
|
||||
|
||||
resultData.value = peopleResults;
|
||||
|
||||
return resultData;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
{
|
||||
"@odata.context": "https://graph.microsoft.com/beta/$metadata#users(id,userPrincipalName,displayName,mail,jobTitle,mobilePhone,companyName,extension_7d1bd45a54af4c7c943ed5bff5a2a231_company)",
|
||||
"@odata.count": 31,
|
||||
"value": [
|
||||
{
|
||||
"id": "87d349ed-44d7-43e1-9a83-5f2406dee5bd",
|
||||
"userPrincipalName": "AdeleV@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Adele Vance",
|
||||
"mail": "AdeleV@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Product Marketing Manager",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "4782e723-f4f4-4af3-a76e-25e3bab0d896",
|
||||
"userPrincipalName": "AlexW@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Alex Wilber",
|
||||
"mail": "AlexW@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Marketing Assistant",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "c03e6eaa-b6ab-46d7-905b-73ec7ea1f755",
|
||||
"userPrincipalName": "AllanD@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Allan Deyoung",
|
||||
"mail": "AllanD@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Corporate Security Officer",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "f5289423-7233-4d60-831a-fe107a8551cc",
|
||||
"userPrincipalName": "BenW@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Ben Walters",
|
||||
"mail": "BenW@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "VP Sales",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "e46ba1a2-59e7-4019-b0fa-b940053e0e30",
|
||||
"userPrincipalName": "BrianJ@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Brian Johnson (TAILSPIN)",
|
||||
"mail": "BrianJ@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "b66ecf79-a093-4d51-86e0-efcc4531f37a",
|
||||
"userPrincipalName": "ChristieC@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Christie Cline",
|
||||
"mail": "ChristieC@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Sr. VP Sales & Marketing",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "6e7b768e-07e2-4810-8459-485f84f8f204",
|
||||
"userPrincipalName": "Adams@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Conf Room Adams",
|
||||
"mail": "Adams@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "013b7b1b-5411-4e6e-bdc9-c4790dae1051",
|
||||
"userPrincipalName": "Baker@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Conf Room Baker",
|
||||
"mail": "Baker@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "8528d6e9-dce3-45d1-85d4-d2db5f738a9f",
|
||||
"userPrincipalName": "Crystal@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Conf Room Crystal",
|
||||
"mail": "Crystal@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "3fec04fc-e036-42f4-8f6f-b3b02288085c",
|
||||
"userPrincipalName": "Hood@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Conf Room Hood",
|
||||
"mail": "Hood@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "6f1c452b-f9f4-4f43-8c42-17e30ab0077c",
|
||||
"userPrincipalName": "Rainier@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Conf Room Rainier",
|
||||
"mail": "Rainier@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "5c7188eb-da70-4f1a-a8a5-afc26c2fe22c",
|
||||
"userPrincipalName": "Stevens@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Conf Room Stevens",
|
||||
"mail": "Stevens@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "d4957c9d-869e-4364-830c-d0c95be72738",
|
||||
"userPrincipalName": "DebraB@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Debra Berger",
|
||||
"mail": "DebraB@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Administrative Assistant",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "24fcbca3-c3e2-48bf-9ffc-c7f81b81483d",
|
||||
"userPrincipalName": "DiegoS@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Diego Siciliani",
|
||||
"mail": "DiegoS@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "CVP Finance",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "2804bc07-1e1f-4938-9085-ce6d756a32d2",
|
||||
"userPrincipalName": "EmilyB@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Emily Braun",
|
||||
"mail": "EmilyB@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Budget Analyst",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "16cfe710-1625-4806-9990-91b8f0afee35",
|
||||
"userPrincipalName": "EnricoC@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Enrico Cattaneo",
|
||||
"mail": "EnricoC@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Attorney",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "df043ff1-49d5-414e-86a4-0c7f239c36cf",
|
||||
"userPrincipalName": "GradyA@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Grady Archie",
|
||||
"mail": "GradyA@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "CVP Legal",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "c8913c86-ceea-4d39-b1ea-f63a5b675166",
|
||||
"userPrincipalName": "HenriettaM@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Henrietta Mueller",
|
||||
"mail": "HenriettaM@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Marketing Assistant",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "baafca12-9874-4765-9576-e0e5cafe491b",
|
||||
"userPrincipalName": "IrvinS@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Irvin Sayers",
|
||||
"mail": "IrvinS@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Director",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "e3d0513b-449e-4198-ba6f-bd97ae7cae85",
|
||||
"userPrincipalName": "IsaiahL@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Isaiah Langer",
|
||||
"mail": "IsaiahL@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Web Marketing Manager",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "626cbf8c-5dde-46b0-8385-9e40d64736fe",
|
||||
"userPrincipalName": "JohannaL@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Johanna Lorenz",
|
||||
"mail": "JohannaL@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "CVP Engineering",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "8b209ac8-08ff-4ef1-896d-3b9fde0bbf04",
|
||||
"userPrincipalName": "JoniS@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Joni Sherman",
|
||||
"mail": "JoniS@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Paralegal",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "074e56ea-0b50-4461-89e5-c67ae14a2c0b",
|
||||
"userPrincipalName": "LeeG@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Lee Gu",
|
||||
"mail": "LeeG@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "CVP Research & Development",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "2ed03dfd-01d8-4005-a9ef-fa8ee546dc6c",
|
||||
"userPrincipalName": "LidiaH@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Lidia Holloway",
|
||||
"mail": "LidiaH@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Product Manager",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "e8a02cc7-df4d-4778-956d-784cc9506e5a",
|
||||
"userPrincipalName": "LynneR@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Lynne Robbins",
|
||||
"mail": "LynneR@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Product Manager",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "48d31887-5fad-4d73-a9f5-3c356e68a038",
|
||||
"userPrincipalName": "MeganB@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Megan Bowen",
|
||||
"mail": "MeganB@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Auditor",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "08fa38e4-cbfa-4488-94ed-c834da6539df",
|
||||
"userPrincipalName": "MiriamG@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Miriam Graham",
|
||||
"mail": "MiriamG@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "VP Marketing",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "5bde3e51-d13b-4db1-9948-fe4b109d11a7",
|
||||
"userPrincipalName": "admin@M365x214355.onmicrosoft.com",
|
||||
"displayName": "MOD Administrator",
|
||||
"mail": "admin@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": null,
|
||||
"mobilePhone": "5555555555",
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "089a6bb8-e8cb-492c-aa41-c078aa0b5120",
|
||||
"userPrincipalName": "NestorW@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Nestor Wilke",
|
||||
"mail": "NestorW@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "CVP Operations",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "40079818-3808-4585-903b-02605f061225",
|
||||
"userPrincipalName": "PattiF@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Patti Fernandez",
|
||||
"mail": "PattiF@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "President",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
},
|
||||
{
|
||||
"id": "ec63c778-24e1-4240-bea3-d12a167d5232",
|
||||
"userPrincipalName": "PradeepG@M365x214355.onmicrosoft.com",
|
||||
"displayName": "Pradeep Gupta",
|
||||
"mail": "PradeepG@M365x214355.onmicrosoft.com",
|
||||
"jobTitle": "Accountant II",
|
||||
"mobilePhone": null,
|
||||
"companyName": null
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { ISearchService } from "./ISearchService";
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { MSGraphClientFactory } from '@microsoft/sp-http';
|
||||
import { isEmpty } from "@microsoft/sp-lodash-subset";
|
||||
import { PageCollection } from "../../models/PageCollection";
|
||||
|
||||
export class SearchService implements ISearchService {
|
||||
private _msGraphClientFactory: MSGraphClientFactory;
|
||||
private _selectParameter: string[];
|
||||
private _filterParameter: string;
|
||||
private _orderByParameter: string;
|
||||
private _searchParameter: string;
|
||||
private _pageSize: number;
|
||||
|
||||
public get selectParameter(): string[] { return this._selectParameter; }
|
||||
public set selectParameter(value: string[]) { this._selectParameter = value; }
|
||||
|
||||
public get filterParameter(): string { return this._filterParameter; }
|
||||
public set filterParameter(value: string) { this._filterParameter = value; }
|
||||
|
||||
public get orderByParameter(): string { return this._orderByParameter; }
|
||||
public set orderByParameter(value: string) { this._orderByParameter = value; }
|
||||
|
||||
public get searchParameter(): string { return this._searchParameter; }
|
||||
public set searchParameter(value: string) { this._searchParameter = value; }
|
||||
|
||||
public get pageSize(): number { return this._pageSize; }
|
||||
public set pageSize(value: number) { this._pageSize = value; }
|
||||
|
||||
constructor(msGraphClientFactory: MSGraphClientFactory) {
|
||||
this._msGraphClientFactory = msGraphClientFactory;
|
||||
}
|
||||
|
||||
public async searchUsers(): Promise<PageCollection<MicrosoftGraph.User>> {
|
||||
const graphClient = await this._msGraphClientFactory.getClient();
|
||||
|
||||
let resultQuery = graphClient
|
||||
.api('/users')
|
||||
.version("beta")
|
||||
.header("ConsistencyLevel", "eventual")
|
||||
.count(true)
|
||||
.top(this.pageSize);
|
||||
|
||||
if (!isEmpty(this.selectParameter)) {
|
||||
resultQuery = resultQuery.select(this.selectParameter);
|
||||
}
|
||||
|
||||
if (!isEmpty(this.filterParameter)) {
|
||||
resultQuery = resultQuery.filter(this.filterParameter);
|
||||
}
|
||||
|
||||
if (!isEmpty(this.orderByParameter)) {
|
||||
resultQuery = resultQuery.orderby(this.orderByParameter);
|
||||
}
|
||||
|
||||
if (!isEmpty(this.searchParameter)) {
|
||||
resultQuery = resultQuery.query({ $search: `"displayName:${this.searchParameter}"` });
|
||||
}
|
||||
|
||||
return await resultQuery.get();
|
||||
}
|
||||
|
||||
public async fetchPage(pageLink: string): Promise<PageCollection<MicrosoftGraph.User>> {
|
||||
const graphClient = await this._msGraphClientFactory.getClient();
|
||||
|
||||
let resultQuery = graphClient.api(pageLink).header("ConsistencyLevel", "eventual");
|
||||
|
||||
return await resultQuery.get();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { ISearchService } from './ISearchService';
|
||||
export { MockSearchService } from './MockSearchService';
|
||||
export { SearchService } from './SearchService';
|
|
@ -0,0 +1,181 @@
|
|||
import * as React from 'react';
|
||||
import ResultsLayoutOption from '../../models/ResultsLayoutOption';
|
||||
import { IPropertyPaneField } from '@microsoft/sp-property-pane';
|
||||
import { PropertyFieldCollectionData, CustomCollectionFieldType } from '@pnp/spfx-property-controls/lib/PropertyFieldCollectionData';
|
||||
import * as strings from 'PeopleSearchWebPartStrings';
|
||||
import { PropertyPaneChoiceGroup } from "@microsoft/sp-property-pane";
|
||||
import { IPeopleSearchWebPartProps } from '../../webparts/peoplesearch/IPeopleSearchWebPartProps';
|
||||
import { DebugViewComponent, IDebugViewProps } from '../../components/DebugViewComponent';
|
||||
import ITemplateContext from '../../models/ITemplateContext';
|
||||
import { PeopleViewComponent, IPeopleViewProps } from '../../components/PeopleViewComponent/PeopleViewComponent';
|
||||
import { IPeopleShimmerViewProps, PeopleShimmerViewComponent } from '../../components/PeopleViewComponent/PeopleShimmerViewComponent';
|
||||
|
||||
const PEOPLE_RESULT_SOURCEID = 'b09a7990-05ea-4af9-81ef-edfab16c4e31';
|
||||
|
||||
export interface IComponentFieldsConfiguration {
|
||||
|
||||
/**
|
||||
* The name of the field
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The field name for the inner component props
|
||||
*/
|
||||
field: string;
|
||||
|
||||
/**
|
||||
* The value of the field
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class TemplateService {
|
||||
/**
|
||||
* Gets template parameters
|
||||
* @param layout the selected layout
|
||||
* @param properties the Web Part properties
|
||||
* @param onUpdateAvailableProperties callback when the list of managed properties is fetched by the control (Optional)
|
||||
* @param availableProperties the list of available managed properties already fetched once (Optional)
|
||||
*/
|
||||
public getTemplateParameters(layout: ResultsLayoutOption, properties: IPeopleSearchWebPartProps): IPropertyPaneField<any>[] {
|
||||
|
||||
switch (layout) {
|
||||
case ResultsLayoutOption.People:
|
||||
return this._getPeopleLayoutFields(properties);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public getTemplateComponent(layout: ResultsLayoutOption, results: ITemplateContext): JSX.Element {
|
||||
let templateComponent = null;
|
||||
switch (layout) {
|
||||
case ResultsLayoutOption.People:
|
||||
templateComponent = React.createElement(
|
||||
PeopleViewComponent,
|
||||
{
|
||||
templateContext: results
|
||||
} as IPeopleViewProps
|
||||
);
|
||||
break;
|
||||
case ResultsLayoutOption.Debug:
|
||||
templateComponent = React.createElement(
|
||||
DebugViewComponent,
|
||||
{
|
||||
content: JSON.stringify(results.items, undefined, 2)
|
||||
} as IDebugViewProps
|
||||
);
|
||||
break;
|
||||
}
|
||||
return templateComponent;
|
||||
}
|
||||
|
||||
public getShimmerTemplateComponent(layout: ResultsLayoutOption, results: ITemplateContext): JSX.Element {
|
||||
let templateComponent = null;
|
||||
switch (layout) {
|
||||
case ResultsLayoutOption.People:
|
||||
templateComponent = React.createElement(
|
||||
PeopleShimmerViewComponent,
|
||||
{
|
||||
templateContext: results
|
||||
} as IPeopleShimmerViewProps
|
||||
);
|
||||
break;
|
||||
}
|
||||
return templateComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces item field values with field mapping values configuration
|
||||
* @param fieldsConfigurationAsString the fields configuration as stringified object
|
||||
* @param itemAsString the item context as stringified object
|
||||
* @param themeVariant the current theem variant
|
||||
*/
|
||||
public static processFieldsConfiguration<T>(fieldsConfiguration: IComponentFieldsConfiguration[], item: any): T {
|
||||
|
||||
let processedProps = {};
|
||||
|
||||
// Use configuration
|
||||
fieldsConfiguration.map(configuration => {
|
||||
|
||||
let processedValue = item[configuration.value];
|
||||
processedProps[configuration.field] = processedValue;
|
||||
});
|
||||
|
||||
return processedProps as T;
|
||||
}
|
||||
|
||||
private _getPeopleLayoutFields(properties: IPeopleSearchWebPartProps): IPropertyPaneField<any>[] {
|
||||
|
||||
// Setup default values
|
||||
if (!properties.templateParameters.peopleFields) {
|
||||
|
||||
properties.templateParameters.peopleFields = [
|
||||
{ name: 'User Principal Name', field: 'upn', value: "userPrincipalName" },
|
||||
{ name: 'Primary Text', field: 'text', value: "displayName" },
|
||||
{ name: 'Secondary Text', field: 'secondaryText', value: "jobTitle" },
|
||||
{ name: 'Tertiary Text', field: 'tertiaryText', value: "mail" },
|
||||
{ name: 'Optional Text', field: 'optionalText', value: "mobilePhone" }
|
||||
] as IComponentFieldsConfiguration[];
|
||||
}
|
||||
|
||||
if (!properties.templateParameters.personaSize) {
|
||||
properties.templateParameters.personaSize = 14;
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
// Careful, the property names should match the React components props. These will be injected in the Handlebars template context and passed as web component attributes
|
||||
PropertyFieldCollectionData('templateParameters.peopleFields', {
|
||||
manageBtnLabel: strings.TemplateParameters.ManagePeopleFieldsLabel,
|
||||
key: 'templateParameters.peopleFields',
|
||||
panelHeader: strings.TemplateParameters.ManagePeopleFieldsLabel,
|
||||
panelDescription: strings.TemplateParameters.ManagePeopleFieldsPanelDescriptionLabel,
|
||||
enableSorting: false,
|
||||
disableItemCreation: true,
|
||||
disableItemDeletion: true,
|
||||
label: strings.TemplateParameters.ManagePeopleFieldsLabel,
|
||||
value: properties.templateParameters.peopleFields,
|
||||
fields: [
|
||||
{
|
||||
id: 'name',
|
||||
type: CustomCollectionFieldType.string,
|
||||
disableEdit: true,
|
||||
title: strings.TemplateParameters.PlaceholderNameFieldLabel
|
||||
},
|
||||
{
|
||||
id: 'value',
|
||||
type: CustomCollectionFieldType.string,
|
||||
title: strings.TemplateParameters.PlaceholderValueFieldLabel,
|
||||
}
|
||||
]
|
||||
}),
|
||||
PropertyPaneChoiceGroup('templateParameters.personaSize', {
|
||||
label: strings.TemplateParameters.PersonaSizeOptionsLabel,
|
||||
options: [
|
||||
{
|
||||
key: 11,
|
||||
text: strings.TemplateParameters.PersonaSizeExtraSmall
|
||||
},
|
||||
{
|
||||
key: 12,
|
||||
text: strings.TemplateParameters.PersonaSizeSmall
|
||||
},
|
||||
{
|
||||
key: 13,
|
||||
text: strings.TemplateParameters.PersonaSizeRegular
|
||||
},
|
||||
{
|
||||
key: 14,
|
||||
text: strings.TemplateParameters.PersonaSizeLarge
|
||||
},
|
||||
{
|
||||
key: 15,
|
||||
text: strings.TemplateParameters.PersonaSizeExtraLarge
|
||||
}
|
||||
]
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import ResultsLayoutOption from "../../models/ResultsLayoutOption";
|
||||
import { DynamicProperty } from '@microsoft/sp-component-base';
|
||||
|
||||
export interface IPeopleSearchWebPartProps {
|
||||
selectParameter: string;
|
||||
filterParameter: string;
|
||||
orderByParameter: string;
|
||||
searchParameter: DynamicProperty<string>;
|
||||
pageSize: string;
|
||||
showPagination: boolean;
|
||||
showResultsCount: boolean;
|
||||
showBlank: boolean;
|
||||
selectedLayout: ResultsLayoutOption;
|
||||
webPartTitle: string;
|
||||
templateParameters: { [key:string]: any };
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "0890f045-1475-4b52-8e1d-28f9ef6943ff",
|
||||
"alias": "MsGraphPeopleSearchWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart","TeamsTab", "SharePointFullPage"],
|
||||
"supportsThemeVariants": true,
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
|
||||
"group": { "default": "SPFx WebParts" },
|
||||
"title": { "default": "People Search" },
|
||||
"description": { "default": "Search for people within your organization, through Microsoft Graph" },
|
||||
"officeFabricIconFontName": "ProfileSearch",
|
||||
"properties": {
|
||||
"selectParameter": "",
|
||||
"filterParameter": "",
|
||||
"orderByParameter": "",
|
||||
"pageSize": 10,
|
||||
"webPartTitle": "People Search",
|
||||
"showPagination": true,
|
||||
"showBlank": true,
|
||||
"showResultsCount": true
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,373 @@
|
|||
import * as React from "react";
|
||||
import * as ReactDom from "react-dom";
|
||||
import { Version, Environment, EnvironmentType } from "@microsoft/sp-core-library";
|
||||
import { ThemeProvider, IReadonlyTheme, ThemeChangedEventArgs } from '@microsoft/sp-component-base';
|
||||
import { BaseClientSideWebPart, IWebPartPropertiesMetadata } from "@microsoft/sp-webpart-base";
|
||||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
import { isEqual } from '@microsoft/sp-lodash-subset';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneToggle,
|
||||
IPropertyPaneField,
|
||||
IPropertyPaneChoiceGroupOption,
|
||||
PropertyPaneChoiceGroup,
|
||||
PropertyPaneTextField,
|
||||
IPropertyPaneGroup,
|
||||
IPropertyPaneConditionalGroup,
|
||||
DynamicDataSharedDepth,
|
||||
PropertyPaneDynamicField,
|
||||
PropertyPaneDynamicFieldSet
|
||||
} from "@microsoft/sp-property-pane";
|
||||
import * as update from 'immutability-helper';
|
||||
import * as strings from "PeopleSearchWebPartStrings";
|
||||
import { IPeopleSearchWebPartProps } from "./IPeopleSearchWebPartProps";
|
||||
import { ISearchService, MockSearchService, SearchService } from "../../services/SearchService";
|
||||
import { IPeopleSearchContainerProps, PeopleSearchContainer } from "./components/PeopleSearchContainer";
|
||||
import ResultsLayoutOption from "../../models/ResultsLayoutOption";
|
||||
import { TemplateService } from "../../services/TemplateService/TemplateService";
|
||||
|
||||
export default class PeopleSearchWebPart extends BaseClientSideWebPart<IPeopleSearchWebPartProps> {
|
||||
private _searchService: ISearchService;
|
||||
private _templateService: TemplateService;
|
||||
private _placeholder = null;
|
||||
private _themeProvider: ThemeProvider;
|
||||
private _themeVariant: IReadonlyTheme;
|
||||
private _initComplete = false;
|
||||
private _templatePropertyPaneOptions: IPropertyPaneField<any>[] = [];
|
||||
|
||||
public async render(): Promise<void> {
|
||||
|
||||
if (!this._initComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._initTemplate();
|
||||
|
||||
if (this.displayMode === DisplayMode.Edit) {
|
||||
const { Placeholder } = await import(
|
||||
/* webpackChunkName: 'search-property-pane' */
|
||||
'@pnp/spfx-controls-react/lib/Placeholder'
|
||||
);
|
||||
this._placeholder = Placeholder;
|
||||
}
|
||||
|
||||
this.renderCompleted();
|
||||
}
|
||||
|
||||
protected get isRenderAsync(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected renderCompleted(): void {
|
||||
super.renderCompleted();
|
||||
let renderElement = null;
|
||||
|
||||
if (this._isWebPartConfigured()) {
|
||||
|
||||
const searchParameter: string | undefined = this.properties.searchParameter.tryGetValue();
|
||||
|
||||
this._searchService = update(this._searchService, {
|
||||
selectParameter: { $set: this.properties.selectParameter ? this.properties.selectParameter.split(',') : [] },
|
||||
filterParameter: { $set: this.properties.filterParameter },
|
||||
orderByParameter: { $set: this.properties.orderByParameter },
|
||||
searchParameter: { $set: searchParameter },
|
||||
pageSize: { $set: parseInt(this.properties.pageSize) }
|
||||
});
|
||||
|
||||
renderElement = React.createElement(
|
||||
PeopleSearchContainer,
|
||||
{
|
||||
webPartTitle: this.properties.webPartTitle,
|
||||
displayMode: this.displayMode,
|
||||
showBlank: this.properties.showBlank,
|
||||
showResultsCount: this.properties.showResultsCount,
|
||||
showPagination: this.properties.showPagination,
|
||||
searchService: this._searchService,
|
||||
templateService: this._templateService,
|
||||
templateParameters: this.properties.templateParameters,
|
||||
selectedLayout: this.properties.selectedLayout,
|
||||
themeVariant: this._themeVariant,
|
||||
serviceScope: this.context.serviceScope,
|
||||
updateWebPartTitle: (value: string) => {
|
||||
this.properties.webPartTitle = value;
|
||||
}
|
||||
} as IPeopleSearchContainerProps
|
||||
);
|
||||
} else {
|
||||
if (this.displayMode === DisplayMode.Edit) {
|
||||
const placeholder: React.ReactElement<any> = React.createElement(
|
||||
this._placeholder,
|
||||
{
|
||||
iconName: strings.PlaceHolderEditLabel,
|
||||
iconText: strings.PlaceHolderIconText,
|
||||
description: strings.PlaceHolderDescription,
|
||||
buttonLabel: strings.PlaceHolderConfigureBtnLabel,
|
||||
onConfigure: this._setupWebPart.bind(this)
|
||||
}
|
||||
);
|
||||
renderElement = placeholder;
|
||||
} else {
|
||||
renderElement = React.createElement('div', null);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDom.render(renderElement, this.domElement);
|
||||
}
|
||||
|
||||
protected async onInit(): Promise<void> {
|
||||
this._initializeRequiredProperties();
|
||||
|
||||
this._initThemeVariant();
|
||||
|
||||
if (Environment.type === EnvironmentType.Local) {
|
||||
this._searchService = new MockSearchService();
|
||||
} else {
|
||||
this._searchService = new SearchService(this.context.msGraphClientFactory);
|
||||
}
|
||||
|
||||
this._templateService = new TemplateService();
|
||||
|
||||
this._initComplete = true;
|
||||
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse("1.0");
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
|
||||
const templateParametersGroup = this._getTemplateFieldsGroup();
|
||||
|
||||
let propertyPaneGroups: (IPropertyPaneGroup | IPropertyPaneConditionalGroup)[] = [
|
||||
{
|
||||
groupName: strings.QuerySettingsGroupName,
|
||||
groupFields: this._getQueryFields()
|
||||
},
|
||||
{
|
||||
primaryGroup: {
|
||||
groupName: strings.SearchQuerySettingsGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('searchParameter', {
|
||||
label: strings.SearchParameter
|
||||
})
|
||||
]
|
||||
},
|
||||
secondaryGroup: {
|
||||
groupName: strings.SearchQuerySettingsGroupName,
|
||||
groupFields: [
|
||||
|
||||
PropertyPaneDynamicFieldSet({
|
||||
label: strings.SearchParameter,
|
||||
fields: [
|
||||
PropertyPaneDynamicField('searchParameter', {
|
||||
label: strings.SearchParameter
|
||||
})
|
||||
],
|
||||
sharedConfiguration: {
|
||||
depth: DynamicDataSharedDepth.Property
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
// Show the secondary group only if the web part has been
|
||||
// connected to a dynamic data source
|
||||
showSecondaryGroup: !!this.properties.searchParameter.tryGetSource()
|
||||
} as IPropertyPaneConditionalGroup,
|
||||
{
|
||||
groupName: strings.StylingSettingsGroupName,
|
||||
groupFields: this._getStylingFields(),
|
||||
}
|
||||
];
|
||||
|
||||
if (templateParametersGroup) {
|
||||
propertyPaneGroups.push(templateParametersGroup);
|
||||
}
|
||||
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
groups: propertyPaneGroups,
|
||||
displayGroupsAsAccordion: false
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected async onPropertyPaneFieldChanged(propertyPath: string) {
|
||||
if (propertyPath.localeCompare('selectedLayout') === 0) {
|
||||
await this._initTemplate();
|
||||
this.context.propertyPane.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected get propertiesMetadata(): IWebPartPropertiesMetadata {
|
||||
return {
|
||||
'searchParameter': {
|
||||
dynamicPropertyType: 'string'
|
||||
}
|
||||
} as any as IWebPartPropertiesMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the group fields for query options inside the property pane
|
||||
*/
|
||||
private _getQueryFields(): IPropertyPaneField<any>[] {
|
||||
let stylingFields: IPropertyPaneField<any>[] = [
|
||||
PropertyPaneTextField('selectParameter', {
|
||||
label: strings.SelectParameter,
|
||||
multiline: true
|
||||
}),
|
||||
PropertyPaneTextField('filterParameter', {
|
||||
label: strings.FilterParameter,
|
||||
multiline: true
|
||||
}),
|
||||
PropertyPaneTextField('orderByParameter', {
|
||||
label: strings.OrderByParameter,
|
||||
multiline: true
|
||||
}),
|
||||
PropertyPaneTextField('pageSize', {
|
||||
label: strings.PageSizeParameter,
|
||||
value: this.properties.pageSize.toString(),
|
||||
maxLength: 3,
|
||||
deferredValidationTime: 300,
|
||||
onGetErrorMessage: (value: string) => {
|
||||
return this._validateNumber(value);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
return stylingFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the template according to the property pane current configuration
|
||||
* @returns the template content as a string
|
||||
*/
|
||||
private async _initTemplate(): Promise<void> {
|
||||
this._templatePropertyPaneOptions = this._templateService.getTemplateParameters(this.properties.selectedLayout, this.properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the group fields for styling options inside the property pane
|
||||
*/
|
||||
private _getStylingFields(): IPropertyPaneField<any>[] {
|
||||
const layoutOptions = [
|
||||
{
|
||||
iconProps: {
|
||||
officeFabricIconFontName: 'People'
|
||||
},
|
||||
text: strings.PeopleLayoutOption,
|
||||
key: ResultsLayoutOption.People
|
||||
},
|
||||
{
|
||||
iconProps: {
|
||||
officeFabricIconFontName: 'Code'
|
||||
},
|
||||
text: strings.DebugLayoutOption,
|
||||
key: ResultsLayoutOption.Debug
|
||||
}
|
||||
] as IPropertyPaneChoiceGroupOption[];
|
||||
|
||||
let stylingFields: IPropertyPaneField<any>[] = [
|
||||
// PropertyPaneToggle('showPagination', {
|
||||
// label: strings.ShowPaginationControl,
|
||||
// }),
|
||||
PropertyPaneToggle('showBlank', {
|
||||
label: strings.ShowBlankLabel,
|
||||
checked: this.properties.showBlank,
|
||||
}),
|
||||
PropertyPaneToggle('showResultsCount', {
|
||||
label: strings.ShowResultsCountLabel,
|
||||
checked: this.properties.showResultsCount,
|
||||
}),
|
||||
PropertyPaneChoiceGroup('selectedLayout', {
|
||||
label: strings.ResultsLayoutLabel,
|
||||
options: layoutOptions
|
||||
}),
|
||||
];
|
||||
|
||||
return stylingFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets template parameters fields
|
||||
*/
|
||||
private _getTemplateFieldsGroup(): IPropertyPaneGroup {
|
||||
|
||||
let templateFieldsGroup: IPropertyPaneGroup = null;
|
||||
|
||||
if (this._templatePropertyPaneOptions.length > 0) {
|
||||
templateFieldsGroup = {
|
||||
groupFields: this._templatePropertyPaneOptions,
|
||||
isCollapsed: false,
|
||||
groupName: strings.TemplateParameters.TemplateParametersGroupName
|
||||
};
|
||||
}
|
||||
|
||||
return templateFieldsGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all webpart properties have been configured
|
||||
*/
|
||||
private _isWebPartConfigured(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Web Part required properties if there are not present in the manifest (i.e. during an update scenario)
|
||||
*/
|
||||
private _initializeRequiredProperties() {
|
||||
this.properties.selectedLayout = (this.properties.selectedLayout !== undefined && this.properties.selectedLayout !== null) ? this.properties.selectedLayout : ResultsLayoutOption.People;
|
||||
this.properties.templateParameters = this.properties.templateParameters ? this.properties.templateParameters : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes theme variant properties
|
||||
*/
|
||||
private _initThemeVariant(): void {
|
||||
// Consume the new ThemeProvider service
|
||||
this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
|
||||
|
||||
// If it exists, get the theme variant
|
||||
this._themeVariant = this._themeProvider.tryGetTheme();
|
||||
|
||||
// Register a handler to be notified if the theme variant changes
|
||||
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current theme variant reference and re-render.
|
||||
* @param args The new theme
|
||||
*/
|
||||
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
|
||||
if (!isEqual(this._themeVariant, args.theme)) {
|
||||
this._themeVariant = args.theme;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Web Part property pane
|
||||
*/
|
||||
private _setupWebPart() {
|
||||
this.context.propertyPane.open();
|
||||
}
|
||||
|
||||
private _validateNumber(value: string): string {
|
||||
let number = parseInt(value);
|
||||
if (isNaN(number)) {
|
||||
return strings.InvalidNumberIntervalMessage;
|
||||
}
|
||||
if (number < 1 || number > 999) {
|
||||
return strings.InvalidNumberIntervalMessage;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { DisplayMode, ServiceScope } from "@microsoft/sp-core-library";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
import { ISearchService } from "../../../../services/SearchService";
|
||||
import ResultsLayoutOption from "../../../../models/ResultsLayoutOption";
|
||||
import { TemplateService } from "../../../../services/TemplateService/TemplateService";
|
||||
|
||||
export interface IPeopleSearchContainerProps {
|
||||
/**
|
||||
* The web part title
|
||||
*/
|
||||
webPartTitle: string;
|
||||
|
||||
/**
|
||||
* The search data provider instance
|
||||
*/
|
||||
searchService: ISearchService;
|
||||
|
||||
/**
|
||||
* Show the result count and entered keywords
|
||||
*/
|
||||
showResultsCount: boolean;
|
||||
|
||||
/**
|
||||
* Show nothing if no result
|
||||
*/
|
||||
showBlank: boolean;
|
||||
|
||||
showPagination: boolean;
|
||||
|
||||
/**
|
||||
* The current display mode of Web Part
|
||||
*/
|
||||
displayMode: DisplayMode;
|
||||
|
||||
/**
|
||||
* The current selected layout
|
||||
*/
|
||||
selectedLayout: ResultsLayoutOption;
|
||||
|
||||
/**
|
||||
* The current theme variant
|
||||
*/
|
||||
themeVariant: IReadonlyTheme | undefined;
|
||||
|
||||
/**
|
||||
* The template helper instance
|
||||
*/
|
||||
templateService: TemplateService;
|
||||
|
||||
/**
|
||||
* Template parameters from Web Part property pane
|
||||
*/
|
||||
templateParameters: { [key:string]: any };
|
||||
|
||||
serviceScope: ServiceScope;
|
||||
|
||||
updateWebPartTitle: (value: string) => void;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { PageCollection } from '../../../../models/PageCollection';
|
||||
|
||||
export interface IPeopleSearchContainerState {
|
||||
results: PageCollection<MicrosoftGraph.User>;
|
||||
areResultsLoading: boolean;
|
||||
errorMessage: string;
|
||||
hasError: boolean;
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
import * as React from "react";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
import * as strings from "PeopleSearchWebPartStrings";
|
||||
import styles from "../PeopleSearchWebPart.module.scss";
|
||||
|
||||
import { IPeopleSearchContainerProps } from "./IPeopleSearchContainerProps";
|
||||
import { IPeopleSearchContainerState } from "./IPeopleSearchContainerState";
|
||||
|
||||
import {
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
} from "office-ui-fabric-react";
|
||||
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
||||
import { ITheme } from '@uifabric/styling';
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
import ResultsLayoutOption from "../../../../models/ResultsLayoutOption";
|
||||
import { isEqual, isEmpty } from "@microsoft/sp-lodash-subset";
|
||||
import ITemplateContext from "../../../../models/ITemplateContext";
|
||||
|
||||
export class PeopleSearchContainer extends React.Component<IPeopleSearchContainerProps,IPeopleSearchContainerState> {
|
||||
|
||||
constructor(props: IPeopleSearchContainerProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
results: {
|
||||
value: []
|
||||
},
|
||||
areResultsLoading: false,
|
||||
errorMessage: '',
|
||||
hasError: false
|
||||
};
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
await this._fetchPeopleSearchResults();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {IPeopleSearchContainerProps} prevProps
|
||||
* @param {IPeopleSearchContainerState} prevState
|
||||
* @memberof Directory
|
||||
*/
|
||||
public async componentDidUpdate(prevProps: IPeopleSearchContainerProps, prevState: IPeopleSearchContainerState) {
|
||||
if (!isEqual(this.props.searchService, prevProps.searchService)) {
|
||||
await this._fetchPeopleSearchResults();
|
||||
}
|
||||
else if (!isEqual(this.props, prevProps)) {
|
||||
if (this.state.hasError) {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
});
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @returns {React.ReactElement<IPeopleSearchContainerProps>}
|
||||
* @memberof Directory
|
||||
*/
|
||||
public render(): React.ReactElement<IPeopleSearchContainerProps> {
|
||||
|
||||
const areResultsLoading = this.state.areResultsLoading;
|
||||
const items = this.state.results;
|
||||
const hasError = this.state.hasError;
|
||||
const errorMessage = this.state.errorMessage;
|
||||
|
||||
const { semanticColors }: IReadonlyTheme = this.props.themeVariant;
|
||||
|
||||
let renderWebPartTitle: JSX.Element = null;
|
||||
let renderWebPartContent: JSX.Element = null;
|
||||
let renderOverlay: JSX.Element = null;
|
||||
let renderShimmerElements: JSX.Element = null;
|
||||
|
||||
// Loading behavior
|
||||
if (areResultsLoading) {
|
||||
if (!isEmpty(items.value)) {
|
||||
renderOverlay = <div>
|
||||
<Overlay isDarkThemed={false} theme={this.props.themeVariant as ITheme} className={styles.overlay}>
|
||||
<Spinner size={SpinnerSize.medium} />
|
||||
</Overlay>
|
||||
</div>;
|
||||
} else {
|
||||
let templateContext = {
|
||||
items: this.state.results,
|
||||
showPagination: this.props.showPagination,
|
||||
showResultsCount: this.props.showResultsCount,
|
||||
showBlank: this.props.showBlank,
|
||||
themeVariant: this.props.themeVariant,
|
||||
pageSize: this.props.searchService.pageSize,
|
||||
serviceScope: this.props.serviceScope
|
||||
} as ITemplateContext;
|
||||
templateContext = { ...templateContext, ...this.props.templateParameters };
|
||||
|
||||
renderShimmerElements = this.props.templateService.getShimmerTemplateComponent(this.props.selectedLayout, templateContext);
|
||||
}
|
||||
}
|
||||
|
||||
// WebPart title
|
||||
renderWebPartTitle = <WebPartTitle displayMode={this.props.displayMode} title={this.props.webPartTitle} updateProperty={(value: string) => this.props.updateWebPartTitle(value)} />;
|
||||
|
||||
// WebPart content
|
||||
if (isEmpty(items.value) && this.props.showBlank && this.props.selectedLayout !== ResultsLayoutOption.Debug) {
|
||||
if (this.props.displayMode === DisplayMode.Edit) {
|
||||
renderWebPartContent = <MessageBar messageBarType={MessageBarType.info}>{strings.ShowBlankEditInfoMessage}</MessageBar>;
|
||||
}
|
||||
else {
|
||||
renderWebPartTitle = null;
|
||||
}
|
||||
} else {
|
||||
let templateContext = {
|
||||
items: this.state.results,
|
||||
showPagination: this.props.showPagination,
|
||||
showResultsCount: this.props.showResultsCount,
|
||||
showBlank: this.props.showBlank,
|
||||
themeVariant: this.props.themeVariant,
|
||||
pageSize: this.props.searchService.pageSize,
|
||||
serviceScope: this.props.serviceScope
|
||||
} as ITemplateContext;
|
||||
templateContext = { ...templateContext, ...this.props.templateParameters };
|
||||
|
||||
let renderSearchResultTemplate = this.props.templateService.getTemplateComponent(this.props.selectedLayout, templateContext);
|
||||
|
||||
renderWebPartContent =
|
||||
<React.Fragment>
|
||||
{renderOverlay}
|
||||
{renderSearchResultTemplate}
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
// Error Message
|
||||
if (hasError) {
|
||||
renderWebPartContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{backgroundColor: semanticColors.bodyBackground}}>
|
||||
<div className={styles.peopleSearchWebPart}>
|
||||
{renderWebPartTitle}
|
||||
{renderShimmerElements ? renderShimmerElements : renderWebPartContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchPeopleSearchResults(): Promise<void> {
|
||||
try {
|
||||
this.setState({
|
||||
areResultsLoading: true,
|
||||
hasError: false,
|
||||
errorMessage: ""
|
||||
});
|
||||
|
||||
const searchResults = await this.props.searchService.searchUsers();
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
areResultsLoading: false
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
results: {
|
||||
value: []
|
||||
},
|
||||
hasError: true,
|
||||
errorMessage: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { PeopleSearchContainer } from './PeopleSearchContainer';
|
||||
export { IPeopleSearchContainerProps } from './IPeopleSearchContainerProps';
|
||||
export { IPeopleSearchContainerState } from './IPeopleSearchContainerState';
|
|
@ -0,0 +1,15 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.peopleSearchWebPart {
|
||||
min-height: 35px;
|
||||
|
||||
// Needed to avoid overlay overflow
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"DebugLayoutOption": "Debug",
|
||||
"FilterParameter": "$filter parameter value",
|
||||
"InvalidNumberIntervalMessage": "Value needs to be at least 1 and maximum 999",
|
||||
"NoResultMessage": "No results to display",
|
||||
"OrderByParameter": "$orderby parameter value",
|
||||
"PageSizeParameter": "Number of items per page",
|
||||
"PeopleLayoutOption": "People",
|
||||
"PlaceHolderEditLabel": "Edit",
|
||||
"PlaceHolderConfigureBtnLabel": "Configure",
|
||||
"PlaceHolderIconText": "People Search Web Part",
|
||||
"PlaceHolderDescription": "This component displays people search results from Microsoft Graph",
|
||||
"QuerySettingsGroupName": "Query",
|
||||
"ResultsLayoutLabel": "Results layout",
|
||||
"ResultsCount": "{0} results",
|
||||
"SearchParameter": "$search parameter value",
|
||||
"SearchQuerySettingsGroupName": "Search Parameter",
|
||||
"SelectParameter": "$select parameter value",
|
||||
"ShowPaginationControl": "Show pagination",
|
||||
"ShowResultsCountLabel": "Show results count",
|
||||
"ShowBlankLabel": "Show blank if no result",
|
||||
"ShowBlankEditInfoMessage": "No result returned for this query. This Web Part will remain blank in display mode according to parameters.",
|
||||
"StylingSettingsGroupName": "Styling options",
|
||||
"TemplateParameters": {
|
||||
"TemplateParametersGroupName": "Template options",
|
||||
"ManagePeopleFieldsLabel": "Manage persona fields",
|
||||
"ManagePeopleFieldsPanelDescriptionLabel": "Here you can map each field values with the corresponding persona placeholders. You can use either the managed property value directly without any transformation or use an Handlebars expression in the value field.",
|
||||
"PlaceholderNameFieldLabel": "Name",
|
||||
"PlaceholderValueFieldLabel": "Value",
|
||||
"PersonaSizeOptionsLabel": "Picture size",
|
||||
"PersonaSizeExtraSmall": "Extra small",
|
||||
"PersonaSizeSmall": "Small",
|
||||
"PersonaSizeRegular": "Regular",
|
||||
"PersonaSizeLarge": "Large",
|
||||
"PersonaSizeExtraLarge": "Extra large",
|
||||
},
|
||||
}
|
||||
});
|
42
samples/react-msgraph-peoplesearch/src/webparts/peoplesearch/loc/mystrings.d.ts
vendored
Normal file
42
samples/react-msgraph-peoplesearch/src/webparts/peoplesearch/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
declare interface IPeopleSearchWebPartStrings {
|
||||
DebugLayoutOption: string;
|
||||
FilterParameter: string;
|
||||
InvalidNumberIntervalMessage: string;
|
||||
NoResultMessage: string;
|
||||
OrderByParameter: string;
|
||||
PageSizeParameter: string;
|
||||
PeopleLayoutOption: string;
|
||||
PlaceHolderEditLabel: string;
|
||||
PlaceHolderConfigureBtnLabel: string;
|
||||
PlaceHolderIconText: string;
|
||||
PlaceHolderDescription: string;
|
||||
QuerySettingsGroupName: string;
|
||||
ResultsCount: string;
|
||||
ResultsLayoutLabel: string;
|
||||
SearchParameter: string;
|
||||
SearchQuerySettingsGroupName: string;
|
||||
SelectParameter: string;
|
||||
ShowPaginationControl: string;
|
||||
ShowResultsCountLabel: string;
|
||||
ShowBlankLabel: string;
|
||||
ShowBlankEditInfoMessage: string;
|
||||
StylingSettingsGroupName: string;
|
||||
TemplateParameters: {
|
||||
TemplateParametersGroupName: string;
|
||||
ManagePeopleFieldsLabel: string;
|
||||
ManagePeopleFieldsPanelDescriptionLabel: string;
|
||||
PlaceholderNameFieldLabel: string;
|
||||
PlaceholderValueFieldLabel: string;
|
||||
PersonaSizeOptionsLabel: string,
|
||||
PersonaSizeExtraSmall: string;
|
||||
PersonaSizeSmall: string;
|
||||
PersonaSizeRegular: string;
|
||||
PersonaSizeLarge: string;
|
||||
PersonaSizeExtraLarge: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'PeopleSearchWebPartStrings' {
|
||||
const strings: IPeopleSearchWebPartStrings;
|
||||
export = strings;
|
||||
}
|
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,64 @@
|
|||
/**
|
||||
* This script updates the package-solution version analogue to the
|
||||
* the package.json file.
|
||||
*/
|
||||
|
||||
if (process.env.npm_package_version === undefined) {
|
||||
|
||||
throw 'Package version cannot be evaluated';
|
||||
|
||||
}
|
||||
|
||||
// define path to package-solution file
|
||||
const solution = './config/package-solution.json',
|
||||
teams = './teams/manifest.json';
|
||||
|
||||
// require filesystem instance
|
||||
const fs = require('fs');
|
||||
|
||||
// get next automated package version from process variable
|
||||
const nextPkgVersion = process.env.npm_package_version;
|
||||
|
||||
// make sure next build version match
|
||||
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
|
||||
nextPkgVersion : nextPkgVersion.split('-')[0];
|
||||
|
||||
// Update version in SPFx package-solution if exists
|
||||
if (fs.existsSync(solution)) {
|
||||
|
||||
// read package-solution file
|
||||
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
|
||||
// parse file as json
|
||||
const solutionContents = JSON.parse(solutionFileContent);
|
||||
|
||||
// set property of version to next version
|
||||
solutionContents.solution.version = nextVersion + '.0';
|
||||
|
||||
// save file
|
||||
fs.writeFileSync(
|
||||
solution,
|
||||
// convert file back to proper json
|
||||
JSON.stringify(solutionContents, null, 2),
|
||||
'UTF-8');
|
||||
|
||||
}
|
||||
|
||||
// Update version in teams manifest if exists
|
||||
if (fs.existsSync(teams)) {
|
||||
|
||||
// read package-solution file
|
||||
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
|
||||
// parse file as json
|
||||
const teamsContent = JSON.parse(teamsManifestContent);
|
||||
|
||||
// set property of version to next version
|
||||
teamsContent.version = nextVersion;
|
||||
|
||||
// save file
|
||||
fs.writeFileSync(
|
||||
teams,
|
||||
// convert file back to proper json
|
||||
JSON.stringify(teamsContent, null, 2),
|
||||
'UTF-8');
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule" : true,
|
||||
"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",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"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