commit new webpart
This commit is contained in:
parent
57ec332b73
commit
e5c801b953
|
@ -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,73 @@
|
||||||
|
# react-organization-chart
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Short summary on functionality and used technologies.
|
||||||
|
|
||||||
|
[picture of the solution in action, if possible]
|
||||||
|
|
||||||
|
## Used SharePoint Framework Version
|
||||||
|
|
||||||
|
![version](https://img.shields.io/badge/version-1.12-green.svg)
|
||||||
|
|
||||||
|
## Applies to
|
||||||
|
|
||||||
|
- [SharePoint Framework](https://aka.ms/spfx)
|
||||||
|
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||||
|
|
||||||
|
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
> Any special pre-requisites?
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Solution|Author(s)
|
||||||
|
--------|---------
|
||||||
|
folder name | Author details (name, company, twitter alias with link)
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
Version|Date|Comments
|
||||||
|
-------|----|--------
|
||||||
|
1.1|March 10, 2021|Update comment
|
||||||
|
1.0|January 29, 2021|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
|
||||||
|
- Ensure that you are at the solution folder
|
||||||
|
- in the command-line run:
|
||||||
|
- **npm install**
|
||||||
|
- **gulp serve**
|
||||||
|
|
||||||
|
> Include any additional steps as needed.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Description of the extension that expands upon high-level summary above.
|
||||||
|
|
||||||
|
This extension illustrates the following concepts:
|
||||||
|
|
||||||
|
- topic 1
|
||||||
|
- topic 2
|
||||||
|
- topic 3
|
||||||
|
|
||||||
|
> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance.
|
||||||
|
|
||||||
|
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||||
|
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
|
||||||
|
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
|
||||||
|
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
|
||||||
|
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
|
|
@ -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,84 @@
|
||||||
|
// 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'));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
/* build.configureWebpack.setConfig({
|
||||||
|
additionalConfiguration: function (config) {
|
||||||
|
let newConfig = config;
|
||||||
|
config.plugins.forEach((plugin, i) => {
|
||||||
|
if (plugin.options && plugin.options.mangle) {
|
||||||
|
config.plugins.splice(i, 1);
|
||||||
|
newConfig = merge(config, {
|
||||||
|
plugins: [
|
||||||
|
new TerserPlugin()
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
|
}
|
||||||
|
}); */
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
/* generatedConfiguration.externals = generatedConfiguration.externals
|
||||||
|
.filter(name => !(["@fluentui/react"].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