commit new webpart

This commit is contained in:
João Mendes 2021-05-03 20:03:52 +01:00
parent 57ec332b73
commit e5c801b953
46 changed files with 23473 additions and 0 deletions

View File

@ -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"
]
}

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,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"
}
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-organization-chart",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

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

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,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

View File

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

View File

@ -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;
});
};

View File

@ -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>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -0,0 +1,6 @@
import { IUserInfo } from "../../models/IUserInfo";
export interface ICompactCardProps {
user: IUserInfo
}

View File

@ -0,0 +1,6 @@
import { IUserInfo } from "../../models/IUserInfo";
export interface IExpandedCardProps {
user: IUserInfo
}

View File

@ -0,0 +1,5 @@
export * from './CompactCard';
export * from './ExpandedCard';
export * from './ICompactCardProps';
export * from './IExpandedCardProps';
export * from './useHoverCardStyles';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
</>
);
};

View File

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

View File

@ -0,0 +1,6 @@
export * from './EOrgChartTypes';
export * from './IOrgChartProps';
export * from './IOrgChartState';
export * from './OrgChart.module.scss';
export * from './OrgChart';
export * from './OrgChartReducer';

View File

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

View File

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

View File

@ -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 },
}}
/>
</>
);
};

View File

@ -0,0 +1,8 @@
import { IUserInfo } from "../../models";
export interface IPersonCardProps {
userInfo: IUserInfo;
onUserSelected: (user: IUserInfo) => void;
selectedUser?: IUserInfo;
showActionsBar?: boolean;
}

View File

@ -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>
</>
);
};

View File

@ -0,0 +1,2 @@
export * from './PersonCard';
export * from './usePersonaCardStyles';

View File

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

View File

@ -0,0 +1 @@
export * from './useGetUserProperties';

View File

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

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,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;
}

View File

@ -0,0 +1,8 @@
export interface IUser {
displayName: string;
email: string;
isAnonymousGuestUser?: boolean;
isExternalGuestUser?: boolean;
loginName?: string;
preferUserTimeZone?: boolean;
}

View File

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

View File

@ -0,0 +1,3 @@
export * from './IPersonProperties';
export * from './IUser';
export * from './IUserInfo';

View File

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

View File

@ -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,
}),
],
},
],
},
],
};
}
}

View 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"
}
});

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

View File

@ -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"
]
}
}