Merge pull request #1847 from joaojmendes/react-organization-chart
This commit is contained in:
commit
bb5c46ce58
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["*.js"],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended"
|
||||
]
|
||||
}
|
|
@ -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,11 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "onprem19",
|
||||
"version": "1.12.0",
|
||||
"libraryName": "react-organization-chart",
|
||||
"libraryId": "0b4a3e5d-123f-41ea-96c4-538c6a19932b",
|
||||
"packageManager": "npm",
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
# Organization Chart
|
||||
|
||||
## Summary
|
||||
|
||||
This web part shows an organization chart based on specified user, and user can navigate to show company organization. This web part can be installed on SharePoint Server 2019, and SharePoint Online.
|
||||
|
||||
![Organization Chart](./assets/orgchart_02.jpg)
|
||||
|
||||
![Organization Chart](./assets/orgchart_01.jpg)
|
||||
|
||||
![Organization Chart](./assets/orgchart.gif)
|
||||
|
||||
## Compatibility
|
||||
|
||||
![SPFx 1.4.1](https://img.shields.io/badge/SPFx-1.4.1-green.svg)
|
||||
![Node.js LTS 6.x | LTS 8.x](https://img.shields.io/badge/Node.js-LTS%206.x%20%7C%20LTS%208.x-green.svg)
|
||||
![SharePoint 2019 | Online](https://img.shields.io/badge/SharePoint-2019%20%7C%20Online-yellow.svg)
|
||||
![Teams No: Not designed for Microsoft Teams](https://img.shields.io/badge/Teams-No-red.svg "Not designed for Microsoft Teams")
|
||||
![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench")
|
||||
|
||||
## Applies to
|
||||
|
||||
- [SharePoint Framework](https://aka.ms/spfx)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-organisation-chart |[João Mendes](https://github.com/joaojmendes), Storm Technology, ([@joaojmendes](https://twitter.com/joaojmendes))
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|May, 2021|Initial release
|
||||
|
||||
## Prerequisites
|
||||
|
||||
No pre-requisites
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- Ensure that you are at the solution folder
|
||||
- in the command-line run:
|
||||
- `npm install`
|
||||
- `gulp serve`
|
||||
|
||||
## Features
|
||||
|
||||
This web part shows how to use PnPjs, Office-ui-fabric-react to create an Organization Chart
|
||||
|
||||
This extension illustrates the following concepts:
|
||||
|
||||
## 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.**
|
||||
|
||||
## Support
|
||||
|
||||
We do not support samples, but we do use GitHub to track issues and constantly want to improve these samples.
|
||||
|
||||
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=bug-report.yml&sample=react-organisation-chart&authors=@joaojmendes&title=react-organisation-chart%20-%20).
|
||||
|
||||
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=question.yml&sample=react-organisation-chart&authors=@joaojmendes&title=react-organisation-chart%20-%20).
|
||||
|
||||
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=suggestion.yml&sample=react-organisation-chart&authors=@joaojmendes&title=react-organisation-chart%20-%20).
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-organization-chart" />
|
Binary file not shown.
After Width: | Height: | Size: 10 MiB |
Binary file not shown.
After Width: | Height: | Size: 254 KiB |
Binary file not shown.
After Width: | Height: | Size: 238 KiB |
|
@ -0,0 +1,65 @@
|
|||
[
|
||||
{
|
||||
"name": "pnp-sp-dev-spfx-web-parts-react-organization-chart",
|
||||
"source": "pnp",
|
||||
"title": "Organization Chart (SP2019 and Online)",
|
||||
"shortDescription": "Shows an organization chart based on specified user, and user can navigate to show company organization",
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-organization-chart",
|
||||
"longDescription": [
|
||||
"This web part shows an organization chart based on specified user, and user can navigate to show company organization.",
|
||||
"Can be installed on SharePoint Server 2019, and SharePoint Online."
|
||||
],
|
||||
"creationDateTime": "2021-05-03",
|
||||
"updateDateTime": "2021-05-03",
|
||||
"products": [
|
||||
"SharePoint",
|
||||
"Office"
|
||||
],
|
||||
"metadata": [
|
||||
{
|
||||
"key": "CLIENT-SIDE-DEV",
|
||||
"value": "React"
|
||||
},
|
||||
{
|
||||
"key": "SPFX-VERSION",
|
||||
"value": "1.4.1"
|
||||
}
|
||||
],
|
||||
"thumbnails": [
|
||||
{
|
||||
"type": "image",
|
||||
"order": 100,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-organization-chart/assets/orgchart.gif",
|
||||
"alt": "Web Part Preview"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 101,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-organization-chart/assets/orgchart_01.jpg",
|
||||
"alt": "Web Part Preview"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 102,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-organization-chart/assets/orgchart_02.jpg",
|
||||
"alt": "Web Part Preview"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"gitHubAccount": "joaojmendes",
|
||||
"company": "Storm Technology",
|
||||
"pictureUrl": "https://github.com/joaojmendes.png",
|
||||
"name": "João Mendes",
|
||||
"twitter": "joaojmendes"
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"name": "Build your first SharePoint client-side web part",
|
||||
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
|
||||
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"organization-chart-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/organizationChart/OrganizationChartWebPart.js",
|
||||
"manifest": "./src/webparts/organizationChart/OrganizationChartWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"OrganizationChartWebPartStrings": "lib/webparts/organizationChart/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/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": "react-organization-chart",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-organization-chart",
|
||||
"id": "0b4a3e5d-123f-41ea-96c4-538c6a19932b",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true
|
||||
|
||||
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-organization-chart.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,57 @@
|
|||
// gulpfile.js
|
||||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
const merge = require('webpack-merge');
|
||||
const TerserPlugin = require('terser-webpack-plugin-legacy');
|
||||
build.addSuppression(/Warning - \[sass\] The local CSS class .* is not camelCase and will not be type-safe./gi);
|
||||
|
||||
// force use of projects specified typescript version
|
||||
const typeScriptConfig = require('@microsoft/gulp-core-build-typescript/lib/TypeScriptConfiguration');
|
||||
typeScriptConfig.TypeScriptConfiguration.setTypescriptCompiler(require('typescript'));
|
||||
|
||||
// disable tslint
|
||||
build.tslint.enabled = false;
|
||||
|
||||
const eslint = require('gulp-eslint');
|
||||
|
||||
const eslintSubTask = build.subTask('eslint', function (gulp, buildOptions, done) {
|
||||
return gulp.src(['src/**/*.{ts,tsx}'])
|
||||
// eslint() attaches the lint output to the "eslint" property
|
||||
// of the file object so it can be used by other modules.
|
||||
.pipe(eslint())
|
||||
// eslint.format() outputs the lint results to the console.
|
||||
// Alternatively use eslint.formatEach() (see Docs).
|
||||
.pipe(eslint.format())
|
||||
// To have the process exit with an error code (1) on
|
||||
// lint error, return the stream and pipe to failAfterError last.
|
||||
.pipe(eslint.failAfterError());
|
||||
});
|
||||
|
||||
build.rig.addPreBuildTask(build.task('eslint-task', eslintSubTask));
|
||||
// force use of projects specified react version
|
||||
build.configureWebpack.mergeConfig({
|
||||
|
||||
additionalConfiguration: (generatedConfiguration) => {
|
||||
// force use of projects specified react version
|
||||
generatedConfiguration.externals = generatedConfiguration.externals
|
||||
.filter(name => !(["react", "react-dom"].includes(name)));
|
||||
// force use TerserPlugIn (remove UglifyJs)
|
||||
generatedConfiguration.plugins.forEach((plugin, i) => {
|
||||
if (plugin.options && plugin.options.mangle) {
|
||||
generatedConfiguration.plugins.splice(i, 1);
|
||||
generatedConfiguration = merge(generatedConfiguration, {
|
||||
plugins: [
|
||||
new TerserPlugin()
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
return generatedConfiguration;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "react-organization-chart",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/theme": "^2.1.0",
|
||||
"@microsoft/office-ui-fabric-react-bundle": "^1.11.0",
|
||||
"@microsoft/sp-core-library": "~1.4.1",
|
||||
"@microsoft/sp-lodash-subset": "~1.4.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "^1.11.0",
|
||||
"@microsoft/sp-tslint-rules": "^1.11.0",
|
||||
"@microsoft/sp-webpart-base": "~1.4.1",
|
||||
"@pnp/common": "^1.3.11",
|
||||
"@pnp/logging": "^1.3.11",
|
||||
"@pnp/odata": "^1.3.11",
|
||||
"@pnp/sp": "^1.3.11",
|
||||
"@pnp/spfx-controls-react": "^1.21.1",
|
||||
"@pnp/spfx-property-controls": "^1.3.0",
|
||||
"@uifabric/merge-styles": "^7.19.2",
|
||||
"enhanced-resolve": "^5.8.0",
|
||||
"idb-keyval": "^5.0.5",
|
||||
"office-ui-fabric-react": "^6.214.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"spfx-uifabric-themes": "^0.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.4.1",
|
||||
"@microsoft/sp-module-interfaces": "1.4.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.4.1",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/mocha": "2.2.38",
|
||||
"@types/react": "^16.9.19",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"ajv": "~5.2.2",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"gulp": "~3.9.1",
|
||||
"gulp-eslint": "^6.0.0",
|
||||
"terser-webpack-plugin-legacy": "^1.2.3",
|
||||
"typescript": "3.9.7",
|
||||
"webpack-merge": "^4.2.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import { SPComponentLoader } from "@microsoft/sp-loader";
|
||||
|
||||
// get all the web's regional settings
|
||||
|
||||
const DEFAULT_PERSONA_IMG_HASH = "7ad602295f8386b7615b582d87bcc294";
|
||||
const DEFAULT_IMAGE_PLACEHOLDER_HASH = "4a48f26592f4e1498d7a478a4c48609c";
|
||||
const MD5_MODULE_ID = "8494e7d7-6b99-47b2-a741-59873e42f16f";
|
||||
const PROFILE_IMAGE_URL = "/_layouts/15/userphoto.aspx?size=M&accountname=";
|
||||
|
||||
/**
|
||||
* Gets user photo
|
||||
* @param userId
|
||||
* @returns user photo
|
||||
*/
|
||||
export const getUserPhoto = async (userId: string): Promise<string> => {
|
||||
|
||||
|
||||
const personaImgUrl = PROFILE_IMAGE_URL + userId;
|
||||
|
||||
// tslint:disable-next-line: no-use-before-declare
|
||||
const url: string = await getImageBase64(personaImgUrl);
|
||||
|
||||
const newHash = await getMd5HashForUrl(url);
|
||||
|
||||
if (
|
||||
newHash !== DEFAULT_PERSONA_IMG_HASH &&
|
||||
newHash !== DEFAULT_IMAGE_PLACEHOLDER_HASH
|
||||
) {
|
||||
return "data:image/png;base64," + url;
|
||||
} else {
|
||||
return "undefined";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get MD5Hash for the image url to verify whether user has default image or custom image
|
||||
* @param url
|
||||
*/
|
||||
export const getMd5HashForUrl = async (url: string): Promise<string> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const library : any = await loadSPComponentById(MD5_MODULE_ID) ;
|
||||
try {
|
||||
const md5Hash = library.Md5Hash;
|
||||
if (md5Hash) {
|
||||
const convertedHash = md5Hash(url);
|
||||
return convertedHash;
|
||||
}
|
||||
} catch (error) {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load SPFx component by id, SPComponentLoader is used to load the SPFx components
|
||||
* @param componentId - componentId, guid of the component library
|
||||
*/
|
||||
export const loadSPComponentById = async (
|
||||
componentId: string
|
||||
): Promise<unknown> => {
|
||||
const component: unknown = SPComponentLoader.loadComponentById(componentId)
|
||||
return component;
|
||||
};
|
||||
/**
|
||||
* Gets image base64
|
||||
* @param pictureUrl
|
||||
* @returns image base64
|
||||
*/
|
||||
export const getImageBase64 = async (pictureUrl: string): Promise<string> => {
|
||||
console.log(pictureUrl);
|
||||
return new Promise((resolve) => {
|
||||
const image = new Image();
|
||||
image.addEventListener("load", () => {
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
(tempCanvas.width = image.width),
|
||||
(tempCanvas.height = image.height),
|
||||
tempCanvas.getContext("2d").drawImage(image, 0, 0);
|
||||
let base64Str;
|
||||
try {
|
||||
base64Str = tempCanvas.toDataURL("image/png");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
base64Str = base64Str.replace(/^data:image\/png;base64,/, "");
|
||||
resolve(base64Str);
|
||||
});
|
||||
image.src = pictureUrl;
|
||||
});
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
Stack,
|
||||
DocumentCard,
|
||||
DocumentCardDetails,
|
||||
PersonaSize,
|
||||
DocumentCardActions,
|
||||
IButtonProps,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { Person } from "../Person/Person";
|
||||
import { ICompactCardProps } from "./ICompactCardProps";
|
||||
import { useHoverCardStyles } from "./useHoverCardStyles";
|
||||
|
||||
export const CompactCard: React.FunctionComponent<ICompactCardProps> = (
|
||||
props: ICompactCardProps
|
||||
) => {
|
||||
const { user } = props;
|
||||
const {
|
||||
stackPersonaStyles,
|
||||
hoverCardStyles,
|
||||
buttonStylesHouver,
|
||||
documentCardActionStyles
|
||||
} = useHoverCardStyles();
|
||||
|
||||
const documentCardActionsHouver: IButtonProps[] = React.useMemo(() =>{
|
||||
const actions:IButtonProps[] = [] ;
|
||||
actions.push(
|
||||
{
|
||||
iconProps: { iconName: "Chat" },
|
||||
title: "Chat",
|
||||
styles: buttonStylesHouver,
|
||||
onClick: (ev)=>{
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
window.open(`https://teams.microsoft.com/l/chat/0/0?users=${user.email}&message=Hi ${user.displayName} `,"_blank");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (user?.workPhone){
|
||||
actions.push(
|
||||
{
|
||||
iconProps: { iconName: "Phone" },
|
||||
title: "Call",
|
||||
styles: buttonStylesHouver,
|
||||
onClick: (ev)=> {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
window.open(`CALLTO:${user.workPhone}`,"_blank");
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
return actions;
|
||||
},[buttonStylesHouver, user.displayName, user.email, user.workPhone]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
tokens={{ childrenGap: 10 }}
|
||||
horizontalAlign="start"
|
||||
verticalAlign="center"
|
||||
|
||||
>
|
||||
<DocumentCard className={hoverCardStyles.hoverHeader}>
|
||||
<DocumentCardDetails>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="space-between"
|
||||
styles={stackPersonaStyles}
|
||||
>
|
||||
<Person
|
||||
text={user.displayName}
|
||||
secondaryText={user.title}
|
||||
tertiaryText={user.department}
|
||||
userEmail={user.email}
|
||||
pictureUrl={user.pictureUrl}
|
||||
size={PersonaSize.size72}
|
||||
/>
|
||||
</Stack>
|
||||
<DocumentCardActions actions={documentCardActionsHouver} styles={documentCardActionStyles} />
|
||||
</DocumentCardDetails>
|
||||
</DocumentCard>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
import {
|
||||
Stack,
|
||||
FontIcon,
|
||||
Text,
|
||||
IStackTokens,
|
||||
PersonaSize,
|
||||
Link,
|
||||
} from "office-ui-fabric-react"
|
||||
import * as React from "react";
|
||||
import { Person } from "../Person/Person";
|
||||
import { IExpandedCardProps } from "./IExpandedCardProps";
|
||||
import { useHoverCardStyles } from "./useHoverCardStyles";
|
||||
import {
|
||||
useGetUserProperties,
|
||||
manpingUserProperties,
|
||||
} from "../../hooks/useGetUserProperties";
|
||||
import { IUserInfo } from "../../models/IUserInfo";
|
||||
export const ExpandedCard: React.FunctionComponent<IExpandedCardProps> = (
|
||||
props: IExpandedCardProps
|
||||
) => {
|
||||
const { user } = props;
|
||||
const { expandedCardStackStyle, hoverCardStyles } = useHoverCardStyles();
|
||||
|
||||
const stackFieldTokens: IStackTokens = {
|
||||
childrenGap: 15,
|
||||
|
||||
};
|
||||
|
||||
const { getUserProfile } = useGetUserProperties();
|
||||
const [manager, setManager] = React.useState<IUserInfo>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!user.manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const { currentUserProfile } = await getUserProfile(user.manager);
|
||||
const wManager: IUserInfo = await manpingUserProperties(
|
||||
currentUserProfile
|
||||
);
|
||||
setManager(wManager);
|
||||
})();
|
||||
}, [getUserProfile, user.manager]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack tokens={{ childrenGap: 10 }} styles={expandedCardStackStyle}>
|
||||
<Text variant="medium" style={{ fontWeight: 600 }}>
|
||||
Contact
|
||||
</Text>
|
||||
{
|
||||
user.email && (
|
||||
<>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="start"
|
||||
verticalAlign="center"
|
||||
styles={{root:{padding: 5}}}
|
||||
tokens={stackFieldTokens}
|
||||
>
|
||||
<FontIcon iconName="mail" className={hoverCardStyles.iconStyles} />
|
||||
<Link href={`MAILTO:${user.email}`} target="_blank"
|
||||
data-interception="off">
|
||||
<Text variant="smallPlus">{user.email}</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
<div className={hoverCardStyles.separatorHorizontal}></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{user.workPhone && (
|
||||
<>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="start"
|
||||
verticalAlign="center"
|
||||
tokens={stackFieldTokens}
|
||||
styles={{root:{padding: 5}}}
|
||||
>
|
||||
<FontIcon
|
||||
iconName="Phone"
|
||||
className={hoverCardStyles.iconStyles}
|
||||
/>
|
||||
<Link href={`CALLTO:${user.workPhone}`} target="_blank"
|
||||
data-interception="off">
|
||||
<Text variant="smallPlus">{user.workPhone}</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
<div className={hoverCardStyles.separatorHorizontal}></div>
|
||||
</>
|
||||
)}
|
||||
{user.location && (
|
||||
<>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="start"
|
||||
verticalAlign="center"
|
||||
tokens={stackFieldTokens}
|
||||
styles={{root:{padding: 5}}}
|
||||
>
|
||||
<FontIcon
|
||||
iconName="MapPin"
|
||||
className={hoverCardStyles.iconStyles}
|
||||
/>
|
||||
<Link
|
||||
href={`https://www.bing.com/maps?q=${encodeURIComponent(
|
||||
user.location
|
||||
)}`}
|
||||
target="_blank"
|
||||
data-interception="off"
|
||||
>
|
||||
<Text variant="smallPlus">{user.location}</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
{manager && (
|
||||
<>
|
||||
<Text
|
||||
variant="medium"
|
||||
style={{ fontWeight: 600, marginTop: 25, marginBottom: 10 }}
|
||||
>
|
||||
Reports to
|
||||
</Text>
|
||||
<Person
|
||||
userEmail={manager.email}
|
||||
size={PersonaSize.size48}
|
||||
pictureUrl={manager.pictureUrl}
|
||||
text={manager.displayName}
|
||||
secondaryText={manager.title}
|
||||
></Person>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { IUserInfo } from "../../models/IUserInfo";
|
||||
|
||||
export interface ICompactCardProps {
|
||||
user: IUserInfo
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IUserInfo } from "../../models/IUserInfo";
|
||||
|
||||
export interface IExpandedCardProps {
|
||||
user: IUserInfo
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export * from './CompactCard';
|
||||
export * from './ExpandedCard';
|
||||
export * from './ICompactCardProps';
|
||||
export * from './IExpandedCardProps';
|
||||
export * from './useHoverCardStyles';
|
|
@ -0,0 +1,73 @@
|
|||
import { IButtonStyles, IDocumentCardActionsStyles, IStackStyles, mergeStyles, mergeStyleSets } from "office-ui-fabric-react";
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import Theme from "spfx-uifabric-themes";
|
||||
const currentTheme = window.__themeState__.theme;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useHoverCardStyles = () => {
|
||||
|
||||
const stackPersonaStyles: Partial<IStackStyles> = {
|
||||
root: { padding: 15 },
|
||||
};
|
||||
const stackCompactStyles: Partial<IStackStyles> = {
|
||||
root: { padding: 15 },
|
||||
};
|
||||
|
||||
const documentCardActionStyles: Partial<IDocumentCardActionsStyles> = {
|
||||
root: {
|
||||
width: '100%',
|
||||
marginTop:10,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
backgroundColor: currentTheme.neutralLighter,
|
||||
borderTopWidth: 1,
|
||||
borderTopStyle: "solid",
|
||||
borderTopColor: currentTheme.neutralLight,
|
||||
},
|
||||
};
|
||||
|
||||
const buttonStylesHouver: IButtonStyles = {
|
||||
root: {
|
||||
marginRight: 10,
|
||||
},
|
||||
icon: {
|
||||
fontSize: 16,
|
||||
},
|
||||
iconHovered: {
|
||||
// color: currentTheme.themePrimary,
|
||||
fontWeight: 600,
|
||||
},
|
||||
};
|
||||
|
||||
const expandedCardStackStyle: IStackStyles = {
|
||||
root: {
|
||||
// marginTop: 10,
|
||||
paddingLeft: 25,
|
||||
paddingRight: 25,
|
||||
paddingTop: 15,
|
||||
paddingBottom: 30,
|
||||
backgroundColor: currentTheme.neutralLighterAlt,
|
||||
},
|
||||
};
|
||||
|
||||
const hoverCardStyles = mergeStyleSets({
|
||||
separatorHorizontal: mergeStyles({
|
||||
width: "100%",
|
||||
height:0,
|
||||
borderTopWidth: 1,
|
||||
borderTopStyle: "solid",
|
||||
borderTopColor: currentTheme.neutralLight,
|
||||
}),
|
||||
iconStyles: mergeStyles({
|
||||
fontSize: 16, color: currentTheme.themePrimary
|
||||
}),
|
||||
hoverHeader: mergeStyles({
|
||||
minWidth: '100%',
|
||||
borderStyle: "none",
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
return {stackCompactStyles, documentCardActionStyles, hoverCardStyles, expandedCardStackStyle, buttonStylesHouver, stackPersonaStyles };
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
export enum EOrgChartTypes {
|
||||
'SET_RENDER_MANAGERS' = 'SET_RENDER_MANAGERS',
|
||||
'SET_RENDER_DIRECT_REPORTS' = 'SET_RENDER_DIRECT_REPORTS',
|
||||
'SET_IS_LOADING' = 'SET_IS_LOADING',
|
||||
'SET_HAS_ERROR' = 'SET_HAS_ERROR',
|
||||
'SET_CURRENT_USER' = 'SET_CURRENT_USER',
|
||||
'SET_START_FROM_USER' = 'SET_START_FROM_USER'
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
|
||||
import { IPropertyFieldGroupOrPerson } from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
|
||||
export interface IOrgChartProps {
|
||||
title: string;
|
||||
defaultUser: string;
|
||||
context: WebPartContext;
|
||||
startFromUser: IPropertyFieldGroupOrPerson[];
|
||||
showAllManagers: boolean;
|
||||
showActionsBar:boolean;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { IUserInfo } from "../../models/IUserInfo";
|
||||
export interface IErrorInfo {
|
||||
hasError: boolean;
|
||||
errorMessage: string;
|
||||
}
|
||||
export interface IOrgChartState {
|
||||
isLoading: boolean;
|
||||
error:IErrorInfo;
|
||||
renderManagers:JSX.Element[];
|
||||
renderDirectReports: JSX.Element[];
|
||||
currentUser:IUserInfo;
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.OrgChart {
|
||||
.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);
|
||||
}
|
||||
.treeContainer{
|
||||
height: 1080px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.rst__rowContents {
|
||||
border-radius: 10px;
|
||||
box-shadow: 2px -11px 46px -10px 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
import * as React from "react";
|
||||
import { IOrgChartProps } from "./IOrgChartProps";
|
||||
import { IOrgChartState } from "./IOrgChartState";
|
||||
import { OrgChartReducer } from "./OrgChartReducer";
|
||||
import {
|
||||
useGetUserProperties,
|
||||
manpingUserProperties,
|
||||
} from "../../hooks/useGetUserProperties";
|
||||
import { IStackStyles, Stack } from "office-ui-fabric-react/lib/Stack";
|
||||
import { PersonCard } from "../PersonCard/PersonCard";
|
||||
import { IUserInfo } from "../../models/IUserInfo";
|
||||
import { EOrgChartTypes } from "./EOrgChartTypes";
|
||||
import {
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Overlay,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Text,
|
||||
} from "office-ui-fabric-react";
|
||||
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
|
||||
import { getGUID } from "@pnp/common";
|
||||
import { useOrgChartStyles } from "./useOrgChartStyles";
|
||||
|
||||
const initialState: IOrgChartState = {
|
||||
isLoading: true,
|
||||
renderDirectReports: [],
|
||||
renderManagers: [],
|
||||
error: undefined,
|
||||
currentUser: undefined,
|
||||
};
|
||||
|
||||
const titleStyle: IStackStyles = {
|
||||
root: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
};
|
||||
|
||||
export const OrgChart: React.FunctionComponent<IOrgChartProps> = (
|
||||
props: IOrgChartProps
|
||||
) => {
|
||||
const { getUserProfile } = useGetUserProperties();
|
||||
const [state, dispatch] = React.useReducer(OrgChartReducer, initialState);
|
||||
const { orgChartClasses } = useOrgChartStyles();
|
||||
|
||||
const {
|
||||
renderManagers,
|
||||
renderDirectReports,
|
||||
currentUser,
|
||||
isLoading,
|
||||
error,
|
||||
}: IOrgChartState = state;
|
||||
|
||||
const {
|
||||
context,
|
||||
showAllManagers,
|
||||
startFromUser,
|
||||
showActionsBar,
|
||||
title,
|
||||
}: IOrgChartProps = props;
|
||||
|
||||
|
||||
const startFromUserId: string = React.useMemo(
|
||||
() => startFromUser && startFromUser[0].id,
|
||||
[startFromUser]
|
||||
);
|
||||
const onUserSelected = React.useCallback((selectedUser: IUserInfo) => {
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_CURRENT_USER,
|
||||
payload: selectedUser,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadOrgChart = React.useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async (selectedUser: string): Promise<any> => {
|
||||
const wRenderManagers: JSX.Element[] = [];
|
||||
const wRenderDirectReports: JSX.Element[] = [];
|
||||
|
||||
try {
|
||||
const { managersList, reportsLists } = await getUserProfile(
|
||||
selectedUser,
|
||||
startFromUserId,
|
||||
showAllManagers
|
||||
);
|
||||
if (managersList) {
|
||||
for (const managerInfo of managersList) {
|
||||
wRenderManagers.push(
|
||||
<>
|
||||
<PersonCard
|
||||
key={getGUID()}
|
||||
userInfo={managerInfo}
|
||||
onUserSelected={onUserSelected}
|
||||
selectedUser={currentUser}
|
||||
showActionsBar={showActionsBar}
|
||||
></PersonCard>
|
||||
<div
|
||||
key={getGUID()}
|
||||
className={orgChartClasses.separatorVertical}
|
||||
></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
for (const directReport of reportsLists) {
|
||||
wRenderDirectReports.push(
|
||||
<>
|
||||
<PersonCard
|
||||
key={getGUID()}
|
||||
userInfo={directReport}
|
||||
onUserSelected={onUserSelected}
|
||||
selectedUser={currentUser}
|
||||
showActionsBar={showActionsBar}
|
||||
></PersonCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_HAS_ERROR,
|
||||
payload: { hasError: false, errorMessage: "" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_IS_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_HAS_ERROR,
|
||||
payload: {
|
||||
hasError: true,
|
||||
errorMessage: "error",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { wRenderDirectReports, wRenderManagers };
|
||||
},
|
||||
|
||||
[
|
||||
getUserProfile,
|
||||
startFromUserId,
|
||||
showAllManagers,
|
||||
onUserSelected,
|
||||
currentUser,
|
||||
showActionsBar,
|
||||
orgChartClasses.separatorVertical,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (startFromUserId === undefined) return;
|
||||
if (startFromUserId === ''){
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_IS_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_HAS_ERROR,
|
||||
payload: {
|
||||
hasError: true,
|
||||
errorMessage: "User don't have email defined",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { currentUserProfile } = await getUserProfile(startFromUserId);
|
||||
const wCurrentUser: IUserInfo = await manpingUserProperties(
|
||||
currentUserProfile
|
||||
);
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_CURRENT_USER,
|
||||
payload: wCurrentUser,
|
||||
});
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_HAS_ERROR,
|
||||
payload: { hasError: false, errorMessage: "" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_IS_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_HAS_ERROR,
|
||||
payload: {
|
||||
hasError: true,
|
||||
errorMessage: "error",
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [getUserProfile, startFromUserId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (!currentUser) return;
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_IS_LOADING,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const { wRenderDirectReports, wRenderManagers } = await loadOrgChart(
|
||||
currentUser.id
|
||||
);
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_RENDER_MANAGERS,
|
||||
payload: wRenderManagers,
|
||||
});
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_RENDER_DIRECT_REPORTS,
|
||||
payload: wRenderDirectReports,
|
||||
});
|
||||
dispatch({
|
||||
type: EOrgChartTypes.SET_IS_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
})();
|
||||
}, [currentUser, loadOrgChart]);
|
||||
|
||||
if (!startFromUser) {
|
||||
return (
|
||||
<Placeholder
|
||||
iconName="Edit"
|
||||
iconText="Configure your Organization Chart Web Part"
|
||||
description={"Please configure web part"}
|
||||
buttonLabel="Configure"
|
||||
onConfigure={context.propertyPane.open}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Overlay style={{ height: "100%", position: "fixed" }}>
|
||||
<Stack style={{ height: "100%" }} verticalAlign="center">
|
||||
<Spinner
|
||||
styles={{ root: { zIndex: 9999 } }}
|
||||
size={SpinnerSize.large}
|
||||
label={"loading Organization Chart..."}
|
||||
labelPosition={"bottom"}
|
||||
></Spinner>
|
||||
</Stack>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && error.hasError) {
|
||||
return (
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="center"
|
||||
styles={{root:{padding: 20}}}
|
||||
tokens={{ childrenGap: 10 }}
|
||||
>
|
||||
<MessageBar messageBarType={MessageBarType.error} isMultiline>
|
||||
{error.errorMessage}
|
||||
</MessageBar>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack styles={{root:{padding: 20}}} >
|
||||
<Stack horizontal horizontalAlign="center" styles={titleStyle}>
|
||||
<Text variant="xLarge" block>
|
||||
{title}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack horizontalAlign="center" verticalAlign="center">
|
||||
{renderManagers}
|
||||
<PersonCard
|
||||
key={getGUID()}
|
||||
userInfo={currentUser}
|
||||
onUserSelected={onUserSelected}
|
||||
selectedUser={currentUser}
|
||||
showActionsBar={showActionsBar}
|
||||
></PersonCard>
|
||||
{renderDirectReports.length && (
|
||||
<>
|
||||
<div className={orgChartClasses.separatorVertical}></div>
|
||||
<div className={orgChartClasses.separatorHorizontal}></div>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="center"
|
||||
styles={{root:{padding: 10}}}
|
||||
tokens={{ childrenGap: 15 }}
|
||||
wrap
|
||||
>
|
||||
{renderDirectReports}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { IErrorInfo } from "../../components/OrgChart/IOrgChartState";
|
||||
import { IUserInfo } from "../../models/IUserInfo";
|
||||
import { EOrgChartTypes } from "./EOrgChartTypes";
|
||||
import { IOrgChartState } from "./IOrgChartState";
|
||||
export const OrgChartReducer = (
|
||||
state: IOrgChartState,
|
||||
action: { type: EOrgChartTypes; payload: unknown }
|
||||
):IOrgChartState => {
|
||||
switch (action.type) {
|
||||
case EOrgChartTypes.SET_RENDER_MANAGERS:
|
||||
return { ...state, renderManagers: action.payload as JSX.Element[] };
|
||||
case EOrgChartTypes.SET_RENDER_DIRECT_REPORTS:
|
||||
return { ...state, renderDirectReports: action.payload as JSX.Element[]};
|
||||
case EOrgChartTypes.SET_IS_LOADING:
|
||||
return { ...state, isLoading: action.payload as boolean};
|
||||
case EOrgChartTypes.SET_HAS_ERROR:
|
||||
return { ...state, error: action.payload as IErrorInfo};
|
||||
case EOrgChartTypes.SET_CURRENT_USER:
|
||||
return { ...state, currentUser: action.payload as IUserInfo};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export * from './EOrgChartTypes';
|
||||
export * from './IOrgChartProps';
|
||||
export * from './IOrgChartState';
|
||||
export * from './OrgChart.module.scss';
|
||||
export * from './OrgChart';
|
||||
export * from './OrgChartReducer';
|
|
@ -0,0 +1,33 @@
|
|||
import { mergeStyles, mergeStyleSets } from "office-ui-fabric-react";
|
||||
|
||||
const currentTheme = window.__themeState__.theme;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useOrgChartStyles = () => {
|
||||
|
||||
const orgChartClasses = mergeStyleSets({
|
||||
tilesContainer: mergeStyles({
|
||||
marginBottom: 10,
|
||||
marginTop: 0,
|
||||
gridGap: "10px",
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
}),
|
||||
|
||||
separatorVertical: mergeStyles({
|
||||
height: 25,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderColor: currentTheme.neutralQuaternary,
|
||||
}),
|
||||
|
||||
separatorHorizontal: mergeStyles({
|
||||
width: "100%",
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderColor: currentTheme.neutralQuaternary,
|
||||
}),
|
||||
});
|
||||
|
||||
return { orgChartClasses };
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { PersonaSize } from "office-ui-fabric-react/lib/Persona";
|
||||
export interface IPersonProps {
|
||||
userEmail: string;
|
||||
text: string;
|
||||
secondaryText: string;
|
||||
tertiaryText?: string;
|
||||
pictureUrl?:string;
|
||||
size?: PersonaSize;
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
IPersonaSharedProps,
|
||||
Persona,
|
||||
PersonaSize,
|
||||
} from "office-ui-fabric-react/lib/Persona";
|
||||
import { Text } from "office-ui-fabric-react/lib/Text";
|
||||
import { IPersonProps } from "./IPersonProps";
|
||||
|
||||
export const Person: React.FunctionComponent<IPersonProps> = (
|
||||
props: IPersonProps
|
||||
) => {
|
||||
const { text, secondaryText, userEmail, size, tertiaryText , pictureUrl} = props;
|
||||
|
||||
const personProps: IPersonaSharedProps = React.useMemo(() => {
|
||||
return {
|
||||
imageUrl: pictureUrl ? `/_layouts/15/userphoto.aspx?size=M&accountname=${userEmail}` : undefined,
|
||||
text: text,
|
||||
secondaryText: secondaryText,
|
||||
tertiaryText: tertiaryText,
|
||||
};
|
||||
}, [pictureUrl, userEmail, text, secondaryText, tertiaryText]);
|
||||
|
||||
const _onRenderPrimaryText = React.useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
title={text}
|
||||
variant="mediumPlus"
|
||||
block
|
||||
nowrap
|
||||
styles={{ root: { fontWeight: 600 } }}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}, [text]);
|
||||
|
||||
const _onRenderSecondaryText = React.useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
title={secondaryText}
|
||||
variant="smallPlus"
|
||||
block
|
||||
nowrap
|
||||
styles={{ root: { fontWeight: 400 } }}
|
||||
>
|
||||
{secondaryText}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}, [secondaryText]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Persona
|
||||
{...personProps}
|
||||
size={size || PersonaSize.size40}
|
||||
onRenderPrimaryText={_onRenderPrimaryText}
|
||||
onRenderSecondaryText={_onRenderSecondaryText}
|
||||
styles={{
|
||||
secondaryText: { maxWidth: 230 },
|
||||
primaryText: { maxWidth: 230 },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { IUserInfo } from "../../models";
|
||||
|
||||
export interface IPersonCardProps {
|
||||
userInfo: IUserInfo;
|
||||
onUserSelected: (user: IUserInfo) => void;
|
||||
selectedUser?: IUserInfo;
|
||||
showActionsBar?: boolean;
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/* tslint:disable */
|
||||
import * as React from "react";
|
||||
import {
|
||||
DocumentCard,
|
||||
DocumentCardActions,
|
||||
DocumentCardDetails,
|
||||
IDocumentCard,
|
||||
} from "office-ui-fabric-react/lib/DocumentCard";
|
||||
|
||||
import {
|
||||
Stack,
|
||||
IButtonProps,
|
||||
HoverCard,
|
||||
HoverCardType,
|
||||
IExpandingCardProps,
|
||||
PersonaSize,
|
||||
DirectionalHint,
|
||||
} from "office-ui-fabric-react";
|
||||
import { Person } from "../Person/Person";
|
||||
import { IUserInfo } from "../../models/IUserInfo";
|
||||
import { ExpandedCard, CompactCard } from "../HoverCard";
|
||||
import { usePersonaCardStyles } from "./usePersonaCardStyles";
|
||||
import { IPersonCardProps } from "./IPersonCardProps";
|
||||
|
||||
const currentTheme = window.__themeState__.theme;
|
||||
|
||||
export const PersonCard: React.FunctionComponent<IPersonCardProps> = (
|
||||
props: IPersonCardProps
|
||||
) => {
|
||||
const { userInfo, onUserSelected, showActionsBar } = props;
|
||||
|
||||
const documentCardRef = React.useRef<IDocumentCard>(undefined);
|
||||
const {
|
||||
personaCardStyles,
|
||||
documentCardActionStyles,
|
||||
buttonStyles,
|
||||
stackPersonaStyles,
|
||||
} = usePersonaCardStyles();
|
||||
|
||||
const documentCardActions: IButtonProps[] = React.useMemo(() => {
|
||||
const cardActions: IButtonProps[] = [];
|
||||
|
||||
cardActions.push({
|
||||
iconProps: { iconName: "Chat" },
|
||||
title: "Chat",
|
||||
styles: buttonStyles,
|
||||
onClick: (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
window.open(
|
||||
`https://teams.microsoft.com/l/chat/0/0?users=${userInfo.email}&message=Hi ${userInfo.displayName} `,
|
||||
"_blank"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (userInfo?.email) {
|
||||
cardActions.push({
|
||||
iconProps: { iconName: "Mail" },
|
||||
title: "Mail",
|
||||
styles: buttonStyles,
|
||||
onClick: (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
window.open(`MAILTO:${userInfo.email}`, "_blank");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (userInfo?.workPhone) {
|
||||
cardActions.push({
|
||||
iconProps: { iconName: "Phone" },
|
||||
title: "Call",
|
||||
styles: buttonStyles,
|
||||
onClick: (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
window.open(`CALLTO:${userInfo.workPhone}`, "_blank");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (userInfo.hasDirectReports) {
|
||||
cardActions.push({
|
||||
iconProps: { iconName: "Org" },
|
||||
title: "View Organization",
|
||||
styles: { ...buttonStyles },
|
||||
});
|
||||
}
|
||||
return cardActions;
|
||||
}, [
|
||||
buttonStyles,
|
||||
userInfo.displayName,
|
||||
userInfo.email,
|
||||
userInfo.hasDirectReports,
|
||||
userInfo.workPhone,
|
||||
]);
|
||||
|
||||
const onRenderCompactCard = React.useCallback(
|
||||
(user: IUserInfo): JSX.Element => <CompactCard user={user} />,
|
||||
[]
|
||||
);
|
||||
|
||||
const onRenderExpandedCard = React.useCallback(
|
||||
(user: IUserInfo): JSX.Element => <ExpandedCard user={user} />,
|
||||
[]
|
||||
);
|
||||
|
||||
const expandingCardProps: IExpandingCardProps = React.useMemo(() => {
|
||||
return {
|
||||
onRenderCompactCard: onRenderCompactCard,
|
||||
onRenderExpandedCard: onRenderExpandedCard,
|
||||
renderData: userInfo,
|
||||
directionalHint: DirectionalHint.rightTopEdge,
|
||||
styles: {
|
||||
expandedCard: { backgroundColor: currentTheme.neutralLighterAlt },
|
||||
},
|
||||
gapSpace: 5,
|
||||
calloutProps: {
|
||||
isBeakVisible: false,
|
||||
},
|
||||
};
|
||||
}, [onRenderCompactCard, onRenderExpandedCard, userInfo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentCard
|
||||
className={personaCardStyles.tile}
|
||||
componentRef={documentCardRef}
|
||||
onClick={() => {
|
||||
// documentCardRef.current.focus();
|
||||
if (userInfo.hasDirectReports) {
|
||||
onUserSelected(userInfo);
|
||||
//
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HoverCard
|
||||
expandingCardProps={expandingCardProps}
|
||||
type={HoverCardType.expanding}
|
||||
>
|
||||
<DocumentCardDetails>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="space-between"
|
||||
styles={stackPersonaStyles}
|
||||
>
|
||||
<Person
|
||||
text={userInfo.displayName}
|
||||
secondaryText={userInfo.title}
|
||||
userEmail={userInfo.email}
|
||||
pictureUrl={userInfo.pictureUrl}
|
||||
size={PersonaSize.size40}
|
||||
/>
|
||||
</Stack>
|
||||
</DocumentCardDetails>
|
||||
</HoverCard>
|
||||
{showActionsBar && (
|
||||
<DocumentCardActions
|
||||
actions={documentCardActions}
|
||||
styles={documentCardActionStyles}
|
||||
/>
|
||||
)}
|
||||
</DocumentCard>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from './PersonCard';
|
||||
export * from './usePersonaCardStyles';
|
|
@ -0,0 +1,101 @@
|
|||
import { IButtonStyles, IDocumentCardActionsStyles, IStackStyles, mergeStyles, mergeStyleSets } from "office-ui-fabric-react";
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import Theme from "spfx-uifabric-themes";
|
||||
const currentTheme = window.__themeState__.theme;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const usePersonaCardStyles = () => {
|
||||
|
||||
const stackPersonaStyles: Partial<IStackStyles> = {
|
||||
root: { padding: 15 },
|
||||
};
|
||||
|
||||
const buttonStyles: IButtonStyles = {
|
||||
icon: {
|
||||
fontSize: 12,
|
||||
},
|
||||
iconHovered: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
};
|
||||
|
||||
const documentCardActionStyles: Partial<IDocumentCardActionsStyles> = {
|
||||
root: {
|
||||
height: 34,
|
||||
padding: 0,
|
||||
backgroundColor: currentTheme.neutralLighterAlt,
|
||||
borderTopWidth: 1,
|
||||
borderTopStyle: "solid",
|
||||
borderTopColor: currentTheme.neutralLight,
|
||||
width: "100%",
|
||||
},
|
||||
};
|
||||
|
||||
const personaCardStyles = mergeStyleSets({
|
||||
separatorHorizontal: mergeStyles({
|
||||
width: "100%",
|
||||
borderWidth: 0.5,
|
||||
borderStyle: "solid",
|
||||
borderColor: currentTheme.neutralLight,
|
||||
}),
|
||||
iconStyles: mergeStyles({
|
||||
fontSize: 16, color: currentTheme.themePrimary
|
||||
}),
|
||||
hoverHeader: mergeStyles({
|
||||
minWidth: 260,
|
||||
maxWidth: 260,
|
||||
borderStyle: "none",
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
}),
|
||||
tileCurrentUser: mergeStyles({
|
||||
minWidth: 260,
|
||||
maxWidth: '260px !important',
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRadius: 0,
|
||||
|
||||
borderColor: currentTheme.themePrimary,
|
||||
boxShadow: "0 5px 15px rgba(50, 50, 90, .1)",
|
||||
}),
|
||||
tile: mergeStyles({
|
||||
minWidth: 260,
|
||||
maxWidth: '260px !important',
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderRadius: 0,
|
||||
borderColor: currentTheme.neutralQuaternaryAlt,
|
||||
backgroundColor: currentTheme.white,
|
||||
boxShadow: "0 5px 15px rgba(50, 50, 90, .1)",
|
||||
selectors: {
|
||||
":hover": {
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderLeftStyle: "solid",
|
||||
borderRadius: 0,
|
||||
borderColor: currentTheme.themePrimary,
|
||||
// borderColor: props.color,
|
||||
// borderTopWidth: 2,
|
||||
},
|
||||
":focus": {
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderLeftStyle: "solid",
|
||||
borderRadius: 0,
|
||||
borderColor: currentTheme.themePrimary,
|
||||
// borderTopWidth: 2,
|
||||
},
|
||||
"@media(max-width : 480px)": {
|
||||
maxWidth: "100%",
|
||||
minWidth: "100%",
|
||||
},
|
||||
"@media((min-width : 481px) and (max-width : 12480px))": {
|
||||
maxWidth: 260,
|
||||
minWidth: "50%",
|
||||
},
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
return {documentCardActionStyles, personaCardStyles, buttonStyles, stackPersonaStyles };
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './useGetUserProperties';
|
|
@ -0,0 +1,144 @@
|
|||
|
||||
import { sp, SPBatch} from "@pnp/sp/";
|
||||
import { IUserInfo } from "../models/IUserInfo";
|
||||
import * as React from "react";
|
||||
import { get, set } from "idb-keyval";
|
||||
import { sortBy, filter } from "lodash";
|
||||
import { IPersonProperties } from "../models/IPersonProperties";
|
||||
|
||||
/*************************************************************************************/
|
||||
// Hook to get user profile information
|
||||
// *************************************************************************************/
|
||||
|
||||
type getUserProfileFunc = ( currentUser: string,
|
||||
startUser?: string,
|
||||
showAllManagers?: boolean) => Promise<returnProfileData>;
|
||||
|
||||
type returnProfileData = { managersList:IUserInfo[], reportsLists:IUserInfo[], currentUserProfile :IPersonProperties} ;
|
||||
|
||||
export const useGetUserProperties = (): { getUserProfile:getUserProfileFunc } => {
|
||||
|
||||
const getUserProfile = React.useCallback(
|
||||
async (
|
||||
currentUser: string,
|
||||
startUser?: string,
|
||||
showAllManagers?: boolean
|
||||
): Promise<returnProfileData> => {
|
||||
if (!currentUser) return;
|
||||
const loginName = currentUser;
|
||||
const loginNameStartUser: string = startUser && startUser;
|
||||
const cacheCurrentUser:IPersonProperties = await get(`${loginName}__orgchart__`);
|
||||
let currentUserProfile:IPersonProperties = undefined;
|
||||
if (!cacheCurrentUser) {
|
||||
currentUserProfile = await sp.profiles.getPropertiesFor(loginName);
|
||||
// console.log(currentUserProfile);
|
||||
await set(`${loginName}__orgchart__`, currentUserProfile);
|
||||
} else {
|
||||
currentUserProfile = cacheCurrentUser;
|
||||
}
|
||||
// get Managers and Direct Reports
|
||||
let reportsLists: IUserInfo[] = [];
|
||||
let managersList: IUserInfo[] = [];
|
||||
|
||||
const wDirectReports: string[] =
|
||||
currentUserProfile && currentUserProfile.DirectReports;
|
||||
const wExtendedManagers: string[] =
|
||||
currentUserProfile && currentUserProfile.ExtendedManagers;
|
||||
|
||||
// Get Direct Reports if exists
|
||||
if (wDirectReports && wDirectReports.length > 0) {
|
||||
reportsLists = await getDirectReports(wDirectReports);
|
||||
}
|
||||
// Get Managers if exists
|
||||
if (startUser && wExtendedManagers && wExtendedManagers.length > 0) {
|
||||
managersList = await getExtendedManagers(
|
||||
wExtendedManagers,
|
||||
loginNameStartUser,
|
||||
showAllManagers
|
||||
);
|
||||
}
|
||||
|
||||
return { managersList, reportsLists, currentUserProfile } ;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return { getUserProfile } ;
|
||||
};
|
||||
|
||||
const getDirectReports = async (
|
||||
directReports: string[]
|
||||
): Promise<IUserInfo[]> => {
|
||||
const _reportsList: IUserInfo[] = [];
|
||||
const batch: SPBatch = sp.createBatch();
|
||||
for (const userReport of directReports) {
|
||||
const cacheDirectReport: IPersonProperties = await get(`${userReport}__orgchart__`);
|
||||
if (!cacheDirectReport) {
|
||||
sp.profiles
|
||||
.inBatch(batch)
|
||||
.getPropertiesFor(userReport)
|
||||
.then(async (directReport: IPersonProperties) => {
|
||||
_reportsList.push(await manpingUserProperties(directReport));
|
||||
await set(`${userReport}__orgchart__`, directReport);
|
||||
});
|
||||
} else {
|
||||
_reportsList.push(await manpingUserProperties(cacheDirectReport));
|
||||
}
|
||||
}
|
||||
await batch.execute();
|
||||
return sortBy(_reportsList, ["displayName"]);
|
||||
};
|
||||
|
||||
const getExtendedManagers = async (
|
||||
extendedManagers: string[],
|
||||
startUser: string,
|
||||
showAllManagers: boolean
|
||||
): Promise<IUserInfo[]> => {
|
||||
const wManagers: IUserInfo[] = [];
|
||||
const batch: SPBatch = sp.createBatch();
|
||||
|
||||
for (const manager of extendedManagers) {
|
||||
if (!showAllManagers && manager !== startUser) {
|
||||
continue;
|
||||
}
|
||||
const cacheManager: IPersonProperties = await get(`${manager}__orgchart__`);
|
||||
if (!cacheManager) {
|
||||
sp.profiles
|
||||
.inBatch(batch)
|
||||
.getPropertiesFor(manager)
|
||||
.then(async (_profile: IPersonProperties) => {
|
||||
wManagers.push(await manpingUserProperties(_profile));
|
||||
await set(`${manager}__orgchart__`, _profile);
|
||||
});
|
||||
} else {
|
||||
wManagers.push(await manpingUserProperties(cacheManager));
|
||||
}
|
||||
}
|
||||
await batch.execute();
|
||||
return wManagers;
|
||||
};
|
||||
|
||||
export const manpingUserProperties = async (
|
||||
userProperties: IPersonProperties
|
||||
): Promise<IUserInfo> => {
|
||||
|
||||
return {
|
||||
displayName: userProperties.DisplayName as string,
|
||||
email: userProperties.Email as string,
|
||||
title: userProperties.Title as string,
|
||||
pictureUrl: userProperties.PictureUrl,
|
||||
id: userProperties.AccountName,
|
||||
userUrl: userProperties.UserUrl,
|
||||
numberDirectReports: userProperties.DirectReports.length,
|
||||
hasDirectReports: userProperties.DirectReports.length > 0 ? true : false,
|
||||
hasPeers: userProperties.Peers.length > 0 ? true : false,
|
||||
numberPeers: userProperties.Peers.length,
|
||||
department: filter(userProperties?.UserProfileProperties,{"Key": "Department"})[0].Value ?? '',
|
||||
workPhone: filter(userProperties?.UserProfileProperties,{"Key": "WorkPhone"})[0].Value ?? '',
|
||||
cellPhone: filter(userProperties?.UserProfileProperties,{"Key": "CellPhone"})[0].Value ?? '',
|
||||
location: filter(userProperties?.UserProfileProperties,{"Key": "SPS-Location"})[0].Value ?? '',
|
||||
office: filter(userProperties?.UserProfileProperties,{"Key": "Office"})[0].Value ?? '',
|
||||
manager: filter(userProperties?.UserProfileProperties,{"Key": "Manager"})[0].Value ?? '',
|
||||
loginName: userProperties.loginName
|
||||
};
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,18 @@
|
|||
export interface IPersonProperties {
|
||||
AccountName: string;
|
||||
DirectReports: string[];
|
||||
DisplayName: string;
|
||||
Email: string;
|
||||
ExtendedManagers: string[];
|
||||
ExtendedReports: string[];
|
||||
IsFollowed: boolean;
|
||||
LatestPost: string;
|
||||
Peers: string[];
|
||||
PersonalSiteHostUrl: string;
|
||||
PersonalUrl: string;
|
||||
PictureUrl: string;
|
||||
Title: string;
|
||||
UserProfileProperties: { Key: string; Value: string; ValueType: string }[];
|
||||
UserUrl: string;
|
||||
loginName: string;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface IUser {
|
||||
displayName: string;
|
||||
email: string;
|
||||
isAnonymousGuestUser?: boolean;
|
||||
isExternalGuestUser?: boolean;
|
||||
loginName?: string;
|
||||
preferUserTimeZone?: boolean;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
/* tslint:disable */
|
||||
import { IUser } from "./IUser";
|
||||
export interface IUserInfo extends IUser {
|
||||
title:string;
|
||||
pictureUrl?: string;
|
||||
userUrl?:string;
|
||||
id?:string;
|
||||
hasDirectReports?:boolean;
|
||||
numberDirectReports?:number;
|
||||
hasPeers?:boolean;
|
||||
numberPeers?:number;
|
||||
manager?:string;
|
||||
department?:string;
|
||||
workPhone?:string;
|
||||
cellPhone?:string;
|
||||
location?:string;
|
||||
office?: string;
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './IPersonProperties';
|
||||
export * from './IUser';
|
||||
export * from './IUserInfo';
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "0338da15-07f9-4a53-b267-d790fb495cca",
|
||||
"alias": "OrganizationChartWebPart",
|
||||
"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,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "SPFx - Custom WebParts" },
|
||||
"title": { "default": "Organization Chart" },
|
||||
"description": { "default": "Show Company Organization Chart" },
|
||||
"officeFabricIconFontName": "Org",
|
||||
"properties": {
|
||||
"title": "Organization Chart",
|
||||
"showAllManagers": true,
|
||||
"showActionsBar": true
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneToggle
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import {
|
||||
PropertyFieldPeoplePicker,
|
||||
PrincipalType,
|
||||
IPropertyFieldGroupOrPerson,
|
||||
} from "@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker";
|
||||
import * as strings from 'OrganizationChartWebPartStrings';
|
||||
import {OrgChart} from '../../components/OrgChart/OrgChart';
|
||||
|
||||
import { IOrgChartProps } from "../../components/OrgChart/IOrgChartProps";
|
||||
import { sp } from "@pnp/sp";
|
||||
export interface IOrganizationChartWebPartProps {
|
||||
title: string;
|
||||
currentUser: string;
|
||||
selectedUser: IPropertyFieldGroupOrPerson[];
|
||||
showAllManagers: boolean;
|
||||
showActionsBar: boolean;
|
||||
}
|
||||
|
||||
export default class OrganizationChartWebPart extends BaseClientSideWebPart<IOrganizationChartWebPartProps> {
|
||||
public async onInit(): Promise<void> {
|
||||
sp.setup({
|
||||
spfxContext: this.context,
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IOrgChartProps > = React.createElement(
|
||||
OrgChart,
|
||||
{
|
||||
title: this.properties.title,
|
||||
defaultUser: this.properties.currentUser,
|
||||
startFromUser: this.properties.selectedUser,
|
||||
showAllManagers: this.properties.showAllManagers,
|
||||
context: this.context,
|
||||
showActionsBar: this.properties.showActionsBar
|
||||
}
|
||||
);
|
||||
|
||||
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("title", {
|
||||
label: strings.TitleFieldLabel,
|
||||
}),
|
||||
PropertyFieldPeoplePicker("selectedUser", {
|
||||
context: this.context,
|
||||
label: strings.startFromUserLabel,
|
||||
initialData: this.properties.selectedUser,
|
||||
key: "peopleFieldId",
|
||||
multiSelect: false,
|
||||
allowDuplicate: false,
|
||||
principalType: [PrincipalType.Users],
|
||||
onPropertyChange: this.onPropertyPaneFieldChanged,
|
||||
properties: this.properties,
|
||||
onGetErrorMessage: null,
|
||||
}),
|
||||
PropertyPaneToggle("showAllManagers", {
|
||||
label: strings.showAllManagers,
|
||||
}),
|
||||
PropertyPaneToggle("showActionsBar", {
|
||||
label: strings.showactionsLabel,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
10
samples/react-organization-chart/src/webparts/organizationChart/loc/en-us.js
vendored
Normal file
10
samples/react-organization-chart/src/webparts/organizationChart/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Company Organization Chart",
|
||||
"BasicGroupName": "Properties",
|
||||
"TitleFieldLabel": "Title",
|
||||
"startFromUserLabel": "Start from user",
|
||||
"showactionsLabel": "Show actions bar",
|
||||
"showAllManagers": "Show all managers"
|
||||
}
|
||||
});
|
13
samples/react-organization-chart/src/webparts/organizationChart/loc/mystrings.d.ts
vendored
Normal file
13
samples/react-organization-chart/src/webparts/organizationChart/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
declare interface IOrganizationChartWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
TitleFieldLabel: string;
|
||||
startFromUserLabel: string;
|
||||
showactionsLabel:string;
|
||||
showAllManagers:string;
|
||||
}
|
||||
|
||||
declare module 'OrganizationChartWebPartStrings' {
|
||||
const strings: IOrganizationChartWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6",
|
||||
"dom",
|
||||
"es2015.collection",
|
||||
"es2015.promise"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue