commit
d15fdce70b
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"whichFolder": "subdir"
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
## Summary
|
||||
|
||||
This web part allows user create a navigation menu , grouped by any column of document library.
|
||||
When the user clicks on the header it dynamically load documents.
|
||||
When the user clicks on the header it dynamically load documents.
|
||||
|
||||
![ListItemsMenu](./assets/ListMenuDocs.gif)
|
||||
|
||||
|
@ -51,6 +51,7 @@ React List Items Menu |[João Mendes](https://github.com/joaojmendes) ([@joaojme
|
|||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0|November 20, 2020|Initial release
|
||||
1.0.1|February 18, 2021|Added support for metadata columns
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"solution": {
|
||||
"name": "react-list-items-menu-client-side-solution",
|
||||
"id": "8b4a758d-a968-4e7c-a949-b42e7dd5ad14",
|
||||
"version": "1.0.0.0",
|
||||
"version": "1.0.1.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
/**
|
||||
* Install Chrome Debugger Extension for Visual Studio Code to debug your components with the
|
||||
* Chrome browser: https://aka.ms/spfx-debugger-extensions
|
||||
*/
|
||||
"version": "0.2.0",
|
||||
"configurations": [{
|
||||
"name": "Local workbench",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://localhost:4321/temp/workbench.html",
|
||||
"webRoot": "${workspaceRoot}",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///.././src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../src/*": "${webRoot}/src/*"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9222"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hosted workbench SPTeck",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://spteck.sharepoint.com/_layouts/workbench.aspx",
|
||||
"webRoot": "${workspaceRoot}",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///.././src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../src/*": "${webRoot}/src/*"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9222",
|
||||
"-incognito"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hosted workbench Fingal Dev",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://fingaldev.sharepoint.com/sites/mms-dev/_layouts/workbench.aspx",
|
||||
"webRoot": "${workspaceRoot}",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///.././src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../src/*": "${webRoot}/src/*",
|
||||
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9222",
|
||||
"-incognito"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hosted workbench Fingal INT",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://fingaluat.sharepoint.com/sites/mms-int/_layouts/workbench.aspx",
|
||||
"webRoot": "${workspaceRoot}",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///.././src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../src/*": "${webRoot}/src/*"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9222",
|
||||
"-incognito"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-list-items-menu",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import * as React from "react";
|
||||
|
||||
import * as strings from "ListItemsMenuWebPartStrings";
|
||||
import {
|
||||
filter,
|
||||
findIndex
|
||||
} from "lodash";
|
||||
import { filter, findIndex, uniqBy } from "lodash";
|
||||
import {
|
||||
Customizer,
|
||||
INavLink,
|
||||
|
@ -18,7 +15,7 @@ import {
|
|||
Nav,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Stack
|
||||
Stack,
|
||||
} from "office-ui-fabric-react";
|
||||
|
||||
import { getFileTypeIconProps } from "@uifabric/file-type-icons";
|
||||
|
@ -92,28 +89,39 @@ export const ListItemsMenu: React.FunctionComponent<IListItemsMenuProps> = (
|
|||
props.fieldName,
|
||||
props.listBaseTemplate
|
||||
);
|
||||
console.log("t1", _groupHeaders);
|
||||
const _field: any = await getField(props.listId, props.fieldName);
|
||||
console.log("f1", _field);
|
||||
for (const groupHeader of _groupHeaders) {
|
||||
let _name = groupHeader[props.fieldName]
|
||||
? groupHeader[props.fieldName]
|
||||
: "Unassigned";
|
||||
let _name: any;
|
||||
if (_field.fieldType === "TaxonomyFieldType") {
|
||||
_name = groupHeader.Metadata.Label ?? "Unassigned";
|
||||
|
||||
if (_name != "Unassigned" && _field.fieldType === "User") {
|
||||
_name = _name[0]?.title;
|
||||
}
|
||||
} if (_field.fieldType === "TaxonomyFieldTypeMulti") {
|
||||
_name = groupHeader.Metadata[0]?.Label ?? "Unassigned";
|
||||
|
||||
if (_name != "Unassigned" && _field.fieldType === "Lookup") {
|
||||
_name = _name[0]?.lookupValue;
|
||||
} else {
|
||||
|
||||
_name = groupHeader[props.fieldName] ?? "Unassigned";
|
||||
|
||||
if (_name != "Unassigned" && _field.fieldType === "User") {
|
||||
_name = _name[0]?.title;
|
||||
}
|
||||
if (_name != "Unassigned" && _field.fieldType === "Lookup") {
|
||||
_name = _name[0]?.lookupValue;
|
||||
}
|
||||
}
|
||||
|
||||
_navLinksGroups.push({
|
||||
name: _name,
|
||||
groupData: groupHeader[props.fieldName],
|
||||
groupData: _name,
|
||||
collapseByDefault: true,
|
||||
|
||||
onHeaderClick: _onGroupHeaderClick,
|
||||
links: [],
|
||||
});
|
||||
// Ensure the groups name are unique!
|
||||
_navLinksGroups = uniqBy( _navLinksGroups, "name");
|
||||
}
|
||||
stateRef.current = {
|
||||
...stateRef.current,
|
||||
|
@ -123,6 +131,7 @@ export const ListItemsMenu: React.FunctionComponent<IListItemsMenuProps> = (
|
|||
listName: _field.fieldScope,
|
||||
navLinkGroups: _navLinksGroups,
|
||||
};
|
||||
|
||||
setState(stateRef.current);
|
||||
} catch (error) {
|
||||
stateRef.current = {
|
||||
|
@ -181,7 +190,7 @@ export const ListItemsMenu: React.FunctionComponent<IListItemsMenuProps> = (
|
|||
name: _groupHeaderItem.FileLeafRef,
|
||||
url: _groupHeaderItem.FileRef,
|
||||
iconProps: {
|
||||
...getFileTypeIconProps({
|
||||
...getFileTypeIconProps({
|
||||
extension: _groupHeaderItem.DocIcon,
|
||||
size: 16,
|
||||
imageFileType: "svg",
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import * as JsStoreWorker from "jsstore/dist/jsstore.worker.commonjs2";
|
||||
window["JsStoreWorker"] = JsStoreWorker;
|
||||
import * as JsStore from "jsstore";
|
||||
import { ITable, DATA_TYPE, IDataBase } from "jsstore";
|
||||
|
||||
// Otherwise due to multiple instance multiple worker will be created.
|
||||
const idbCon = new JsStore.Connection();
|
||||
|
||||
export const useJsStore = () => {
|
||||
const initJsStore = async (
|
||||
dbname: string,
|
||||
tablesSchemas: ITable[]
|
||||
): Promise<JsStore.Connection> => {
|
||||
const dataBase: IDataBase = {
|
||||
name: dbname,
|
||||
tables: tablesSchemas,
|
||||
};
|
||||
idbCon.initDb(dataBase);
|
||||
window[`idbCon_${dbname}`] = idbCon;
|
||||
return idbCon;
|
||||
|
||||
};
|
||||
|
||||
return { initJsStore };
|
||||
};
|
|
@ -56,10 +56,12 @@ export const useList = () => {
|
|||
<RowLimit>1000</RowLimit>
|
||||
</View>`;
|
||||
|
||||
console.log('group',_viewXml )
|
||||
const _groupHeadersResults = await sp.web.lists
|
||||
.getById(listId)
|
||||
.renderListDataAsStream({ ViewXml: _viewXml });
|
||||
|
||||
console.log(_groupHeadersResults.Row);
|
||||
//console.log("groups", _groupHeadersResults.Row);
|
||||
return uniqBy(_groupHeadersResults.Row, groupByField);
|
||||
};
|
||||
|
@ -125,6 +127,8 @@ export const useList = () => {
|
|||
</View>`;
|
||||
}
|
||||
|
||||
console.log(_viewXml);
|
||||
|
||||
const _groupItemsResults = await sp.web.lists
|
||||
.getById(listId)
|
||||
.renderListDataAsStream({ ViewXml: _viewXml });
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"@pnp/generator-spfx": {
|
||||
"framework": "react",
|
||||
"pnpFramework": "reactjs.plus",
|
||||
"pnp-libraries": [
|
||||
"jquery@3",
|
||||
"msgraph",
|
||||
"@pnp/pnpjs",
|
||||
"@pnp/spfx-property-controls",
|
||||
"spfx-uifabric-themes",
|
||||
"lodash",
|
||||
"@pnp/spfx-controls-react",
|
||||
"ouifr@7",
|
||||
"rush@3.7"
|
||||
],
|
||||
"pnp-ci": "azure-preview",
|
||||
"pnp-vetting": [],
|
||||
"spfxenv": "spo",
|
||||
"pnp-testing": [
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"environment": "spo",
|
||||
"framework": "react",
|
||||
"isCreatingSolution": true,
|
||||
"version": "1.11.0",
|
||||
"libraryName": "staff-directory",
|
||||
"libraryId": "89d7389c-be48-41e9-9f72-5eb9a1099c1f",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
page_type: sample
|
||||
products:
|
||||
- office-sp
|
||||
languages:
|
||||
- javascript
|
||||
- typescript
|
||||
extensions:
|
||||
contentType: samples
|
||||
technologies:
|
||||
- SharePoint Framework
|
||||
- Msgraph
|
||||
- Fluent UI React Controls
|
||||
platforms:
|
||||
- React
|
||||
createdDate: 4/9/2020 12:00:00 AM
|
||||
---
|
||||
|
||||
# Staff Directory (Search Directory)
|
||||
|
||||
## Summary
|
||||
|
||||
This web part shows the current user's colleagues, and allows the user to search AD directory, The user can configure the properties to show when expand the user card.
|
||||
|
||||
![staff](./assets/staffdirectory.gif)
|
||||
|
||||
## Compatibility
|
||||
|
||||
![SPFx 1.11](https://img.shields.io/badge/SPFx-1.11.0-green.svg)
|
||||
![Node.js LTS 10.x](https://img.shields.io/badge/Node.js-LTS%2010.x-green.svg)
|
||||
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
|
||||
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
|
||||
![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench")
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
|
||||
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-staffdirectory|João Mendes ([joaojmendes](https://github.com/joaojmendes))
|
||||
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0|February 16, 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
|
||||
|
||||
Please follow all the steps:
|
||||
|
||||
- Clone this repository
|
||||
- in the command line run:
|
||||
- `npm install`
|
||||
- `gulp build`
|
||||
- `gulp bundle --ship`
|
||||
- `gulp package-solution --ship`
|
||||
- Add and deploy package to your tenant's App Catalog
|
||||
- Go to **API Access** - from **SharePoint Admin Center** new experience, and **Approve** the permission to use Microsoft Graph scope **Presence.Read.All**
|
||||
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/staffdirectory" />
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 MiB |
|
@ -0,0 +1,67 @@
|
|||
parameters:
|
||||
name: ''
|
||||
jobs:
|
||||
- job: ${{ parameters.name }}
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
demands:
|
||||
- npm
|
||||
- node.js
|
||||
- java
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm
|
||||
|
||||
steps:
|
||||
- checkout: self
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
checkLatest: true
|
||||
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
key: npm | $(Agent.OS) | package-lock.json
|
||||
path: $(npm_config_cache)
|
||||
cacheHitVar: CACHE_RESTORED
|
||||
- script: npm ci
|
||||
displayName: 'npm ci'
|
||||
|
||||
- task: Gulp@0
|
||||
displayName: 'Bundle project'
|
||||
inputs:
|
||||
targets: bundle
|
||||
arguments: '--ship'
|
||||
|
||||
- script: npm test
|
||||
displayName: 'npm test'
|
||||
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish test results
|
||||
inputs:
|
||||
testResultsFormat: JUnit
|
||||
testResultsFiles: '**/junit.xml'
|
||||
#failTaskOnFailedTests: true #if we want to fail the build on failed unit tests
|
||||
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: 'Publish code coverage results'
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
|
||||
|
||||
- task: Gulp@0
|
||||
displayName: 'Package Solution'
|
||||
inputs:
|
||||
targets: 'package-solution'
|
||||
arguments: '--ship'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)'
|
||||
inputs:
|
||||
Contents: |
|
||||
sharepoint/**/*.sppkg
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: drop'
|
|
@ -0,0 +1,39 @@
|
|||
parameters:
|
||||
# unique name of the job
|
||||
job_name: deploy_sppkg
|
||||
# friendly name of the job
|
||||
display_name: Upload & deploy *.sppkg to SharePoint app catalog
|
||||
# name of target enviroment deploying to
|
||||
target_environment: ''
|
||||
# app catalog scope (tenant|sitecollection)
|
||||
o365cli_app_catalog_scope: 'tenant'
|
||||
variable_group_name: ''
|
||||
jobs:
|
||||
- deployment: ${{ parameters.job_name }}
|
||||
displayName: ${{ parameters.display_name }}
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
environment: ${{ parameters.target_environment }}
|
||||
variables:
|
||||
- group: ${{parameters.variable_group_name}} #o365_user_login, o365_user_password, o365_app_catalog_site_url
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- checkout: none
|
||||
- download: current
|
||||
artifact: drop
|
||||
patterns: '**/*.sppkg'
|
||||
- script: sudo npm install --global @pnp/office365-cli
|
||||
displayName: Install Office365 CLI
|
||||
- script: o365 login $(o365_app_catalog_site_url) --authType password --userName $(o365_user_login) --password $(o365_user_password)
|
||||
displayName: Login to Office365
|
||||
- script: |
|
||||
CMD_GET_SPPKG_NAME=$(find $(Pipeline.Workspace)/drop -name '*.sppkg' -exec basename {} \;)
|
||||
echo "##vso[task.setvariable variable=SpPkgFileName;isOutput=true]${CMD_GET_SPPKG_NAME}"
|
||||
displayName: Get generated *.sppkg filename
|
||||
name: GetSharePointPackage
|
||||
- script: o365 spo app add --filePath "$(Pipeline.Workspace)/drop/sharepoint/solution/$(GetSharePointPackage.SpPkgFileName)" --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }} --overwrite
|
||||
displayName: Upload SharePoint package to Site Collection App Catalog
|
||||
- script: o365 spo app deploy --name $(GetSharePointPackage.SpPkgFileName) --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }}
|
||||
displayName: Deploy SharePoint package
|
|
@ -0,0 +1,26 @@
|
|||
name: $(TeamProject)_$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
|
||||
resources:
|
||||
- repo: self
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- develop
|
||||
|
||||
stages:
|
||||
- stage: build
|
||||
displayName: build
|
||||
jobs:
|
||||
- template: ./azure-pipelines-build-template.yml
|
||||
parameters:
|
||||
name: 'buildsolution'
|
||||
- stage: 'deployqa'
|
||||
# uncomment if you want deployments to occur only for a specific branch
|
||||
#condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
|
||||
jobs:
|
||||
- template: ./azure-pipelines-deploy-template.yml
|
||||
parameters:
|
||||
job_name: deploy_solution
|
||||
target_environment: 'qa'
|
||||
variable_group_name: qa_configuration
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"staff-directory-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/staffDirectory/StaffDirectoryWebPart.js",
|
||||
"manifest": "./src/webparts/staffDirectory/StaffDirectoryWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"StaffDirectoryWebPartStrings": "lib/webparts/staffDirectory/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
{
|
||||
"includeExtensions": [
|
||||
"svg",
|
||||
"png",
|
||||
"jpg"
|
||||
]
|
||||
}
|
|
@ -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": "staff-directory",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"preset":"@voitanos/jest-preset-spfx-react16","rootDir":"../src","collectCoverageFrom":["<rootDir>/**/*.{ts,tsx}","!<rootDir>/**/*.scss.*","!<rootDir>/loc/**/*.*"],"coverageReporters":["text","json","lcov","text-summary","cobertura"],"reporters":["default",["jest-junit",{"suiteName":"jest tests","outputDirectory":"temp/test/junit","outputName":"junit.xml"}]]}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "staff-directory-client-side-solution",
|
||||
"id": "89d7389c-be48-41e9-9f72-5eb9a1099c1f",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Presence.Read.All"
|
||||
}
|
||||
],
|
||||
"developer": {
|
||||
"name": "",
|
||||
"websiteUrl": "",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"mpnId": ""
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/staff-directory.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
// check if gulp dist was called
|
||||
if (process.argv.indexOf('dist') !== -1) {
|
||||
// add ship options to command call
|
||||
process.argv.push('--ship');
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
const gulpSequence = require('gulp-sequence');
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
// Create clean distrubution package
|
||||
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
|
||||
// Create clean development package
|
||||
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Custom Framework Specific gulp tasks
|
||||
*/
|
||||
|
||||
|
||||
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "staff-directory",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"preversion": "node ./tools/pre-version.js",
|
||||
"postversion": "gulp dist",
|
||||
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
|
||||
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.11.0",
|
||||
"@microsoft/sp-lodash-subset": "1.11.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
|
||||
"@microsoft/sp-property-pane": "1.11.0",
|
||||
"@microsoft/sp-webpart-base": "1.11.0",
|
||||
"@pnp/pnpjs": "^2.0.6",
|
||||
"@pnp/spfx-controls-react": "1.19.0",
|
||||
"@pnp/spfx-property-controls": "1.19.0",
|
||||
"@types/jquery": "^3.5.0",
|
||||
"jquery": "^3.5.1",
|
||||
"office-ui-fabric-react": "^7.123.1",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^1.12.0",
|
||||
"@microsoft/rush-stack-compiler-3.7": "^0.6.x",
|
||||
"@microsoft/sp-build-web": "1.11.0",
|
||||
"@microsoft/sp-module-interfaces": "1.11.0",
|
||||
"@microsoft/sp-tslint-rules": "1.11.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.11.0",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/mocha": "2.2.38",
|
||||
"@types/react": "^16.9.43",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"@voitanos/jest-preset-spfx-react16": "^1.3.2",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1",
|
||||
"gulp-sequence": "1.0.0",
|
||||
"jest": "^23.6.0",
|
||||
"jest-junit": "^10.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"spfx-uifabric-themes": "^0.8.0",
|
||||
"typescript": "~3.7.x"
|
||||
},
|
||||
"jest-junit": {
|
||||
"output": "temp/test/junit/junit.xml",
|
||||
"usePathForSuiteName": "true"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
import { IAppContext } from '../common/IAppContext';
|
||||
export const AppContext = React.createContext<IAppContext>(undefined);
|
|
@ -0,0 +1,8 @@
|
|||
import { MSGraphClient } from "@microsoft/sp-http";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
export interface IAppContext {
|
||||
currentUser: any;
|
||||
msGraphClient: MSGraphClient;
|
||||
themeVariant: IReadonlyTheme;
|
||||
}
|
||||
//# sourceMappingURL=IAppContext.d.ts.map
|
|
@ -0,0 +1,15 @@
|
|||
import { PersonaPresence } from "office-ui-fabric-react";
|
||||
|
||||
export interface IPresenceStatus {
|
||||
[key:string]: {presenceStatus:number, presenceStatusLabel:string};
|
||||
}
|
||||
export const presenceStatus:IPresenceStatus[] = [];
|
||||
presenceStatus["Available"] = { presenceStatus: PersonaPresence.online, presenceStatusLabel: "Available"};
|
||||
presenceStatus["AvailableIdle"] = { presenceStatus: PersonaPresence.online, presenceStatusLabel: "Available idle"};
|
||||
presenceStatus["Away"] = { presenceStatus: PersonaPresence.away, presenceStatusLabel: "Away"};
|
||||
presenceStatus["BeRightBack"] = { presenceStatus: PersonaPresence.away, presenceStatusLabel: "Be right back"};
|
||||
presenceStatus["Busy"] = { presenceStatus: PersonaPresence.busy, presenceStatusLabel: "Busy"};
|
||||
presenceStatus["BusyIdle"] = { presenceStatus: PersonaPresence.busy, presenceStatusLabel: "Busy idle"};
|
||||
presenceStatus["DoNotDisturb"] = { presenceStatus: PersonaPresence.dnd, presenceStatusLabel: "Do not disturb"};
|
||||
presenceStatus["Offline"] ={ presenceStatus: PersonaPresence.offline, presenceStatusLabel: "Offline "};
|
||||
presenceStatus["PresenceUnknown"] ={ presenceStatus: PersonaPresence.none, presenceStatusLabel: "Presence Unknown" };
|
|
@ -0,0 +1,16 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
export interface IStaffDirectoryProps {
|
||||
title: string;
|
||||
context: WebPartContext;
|
||||
themeVariant: IReadonlyTheme;
|
||||
displayMode: DisplayMode;
|
||||
maxHeight:number;
|
||||
showBox: boolean;
|
||||
updateProperty: (value: string) => void;
|
||||
refreshInterval: number;
|
||||
updatePresenceStatus:boolean;
|
||||
userAttributes: string[];
|
||||
pageSize:number;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { IUserExtended } from "../../entites/IUserExtended";
|
||||
|
||||
export interface IStaffDirectoryState {
|
||||
isLoading: boolean;
|
||||
listUsers: IUserExtended[];
|
||||
hasError:boolean;
|
||||
errorMessage:string;
|
||||
updateUsersPresence:boolean;
|
||||
nextPageLink:string;
|
||||
isLoadingNextPage:boolean;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.hideScrollBar::-webkit-scrollbar-thumb { background-color: $ms-color-neutralLight; }
|
||||
|
||||
.hideScrollBar::-webkit-scrollbar{
|
||||
width: 5px;
|
||||
}
|
|
@ -0,0 +1,536 @@
|
|||
import * as React from "react";
|
||||
import styles from "./StaffDirectory.module.scss";
|
||||
import { IStaffDirectoryProps } from "./IStaffDirectoryProps";
|
||||
import { IStaffDirectoryState } from "./IStaffDirectoryState";
|
||||
import { escape } from "@microsoft/sp-lodash-subset";
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react";
|
||||
import { MSGraphClient } from "@microsoft/sp-http";
|
||||
import {
|
||||
IStackTokens,
|
||||
mergeStyleSets,
|
||||
IBasePickerStyles,
|
||||
IPersonaProps,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
IPersonaStyleProps,
|
||||
ValidationState,
|
||||
Customizer,
|
||||
Stack,
|
||||
FontIcon,
|
||||
NormalPeoplePicker,
|
||||
ImageFit,
|
||||
Image,
|
||||
Text,
|
||||
ActionButton,
|
||||
Link,
|
||||
ILinkStyles,
|
||||
warnConditionallyRequiredProps,
|
||||
LinkBase,
|
||||
mergeStyles,
|
||||
} from "office-ui-fabric-react";
|
||||
import { IAppContext } from "../../common/IAppContext";
|
||||
import { IUser } from "../../entites/IUser";
|
||||
import {
|
||||
useGetUsersByDepartment,
|
||||
useGetUserId,
|
||||
useSearchUsers,
|
||||
useGetUsersPresence,
|
||||
useGetUsersNextPage,
|
||||
} from "../../hooks/useSearchUsers";
|
||||
import { useInterval } from '../../hooks/useInterval';
|
||||
import { IUserExtended } from "../../entites/IUserExtended";
|
||||
import { presenceStatus } from "../../common/PresenceStatus";
|
||||
import { AppContext } from "../../common/AppContext";
|
||||
import { UserCard } from "../UserCard/UserCard";
|
||||
import { useRef } from "react";
|
||||
import strings from "StaffDirectoryWebPartStrings";
|
||||
|
||||
const imageNoData: string = require("../../../assets/Nodatarafiki.svg");
|
||||
|
||||
|
||||
const stackTokens: IStackTokens = {
|
||||
childrenGap: 10,
|
||||
};
|
||||
// Component Styles
|
||||
const stackStyles = {
|
||||
root: {
|
||||
width: "100%",
|
||||
},
|
||||
};
|
||||
const suggestionProps = {
|
||||
suggestionsHeaderText: "Suggested People",
|
||||
mostRecentlyUsedHeaderText: "Suggested Contacts",
|
||||
noResultsFoundText: "No results found",
|
||||
loadingText: "Loading",
|
||||
showRemoveButtons: false,
|
||||
suggestionsAvailableAlertText: "People Picker Suggestions available",
|
||||
suggestionsContainerAriaLabel: "Suggested contacts",
|
||||
};
|
||||
|
||||
export const StaffDirectory: React.FunctionComponent<IStaffDirectoryProps> = (
|
||||
props: IStaffDirectoryProps
|
||||
) => {
|
||||
const { showBox, maxHeight } = props;
|
||||
|
||||
const styleClasses = mergeStyleSets({
|
||||
webPartTitle: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
|
||||
separator: {
|
||||
paddingLeft: 30,
|
||||
paddingRight: 30,
|
||||
margin: 20,
|
||||
borderBottomStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderBottomColor: props.themeVariant.palette.themeLighter,
|
||||
},
|
||||
|
||||
styleIcon: {
|
||||
maxWidth: 44,
|
||||
minWidth: 44,
|
||||
minHeight: 30,
|
||||
height: 30,
|
||||
borderColor: props.themeVariant.palette.themePrimary,
|
||||
borderRightWidth: 0,
|
||||
borderRightStyle: "none",
|
||||
borderLeftWidth: 1,
|
||||
borderLeftStyle: "solid",
|
||||
borderTopWidth: 1,
|
||||
borderTopStyle: "solid",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomStyle: "solid",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
listContainer: {
|
||||
maxWidth: "100%",
|
||||
overflowY: "auto",
|
||||
|
||||
marginTop: 20,
|
||||
padding: 10,
|
||||
boxShadow: showBox
|
||||
? "0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1)"
|
||||
: "",
|
||||
},
|
||||
});
|
||||
|
||||
const pickerStyles: Partial<IBasePickerStyles> = {
|
||||
root: {
|
||||
width: "100%",
|
||||
maxHeight: 32,
|
||||
minHeight: 32,
|
||||
borderColor: props.themeVariant.palette.themePrimary,
|
||||
},
|
||||
itemsWrapper: {
|
||||
borderColor: props.themeVariant.palette.themePrimary,
|
||||
},
|
||||
text: {
|
||||
borderLeftWidth: 0,
|
||||
minHeight: 32,
|
||||
borderColor: props.themeVariant.palette.themePrimary,
|
||||
selectors: {
|
||||
":focus": {
|
||||
borderColor: props.themeVariant.palette.themePrimary,
|
||||
},
|
||||
":hover": {
|
||||
borderColor: props.themeVariant.palette.themePrimary,
|
||||
},
|
||||
"::after": {
|
||||
borderColor: props.themeVariant.palette.themePrimary,
|
||||
borderWidth: 1,
|
||||
borderLeftWidth: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nextPageStyle: ILinkStyles = {
|
||||
root: {
|
||||
fontWeight: 600,
|
||||
fontSize: props.themeVariant.fonts.mediumPlus.fontSize,
|
||||
selectors: { ":hover": { textDecoration: "underline" } },
|
||||
},
|
||||
};
|
||||
const _appContext = React.useRef<IAppContext>({} as IAppContext);
|
||||
const _msGraphClient = React.useRef<MSGraphClient>();
|
||||
const _currentUser= React.useRef<any>('');
|
||||
const _currentUserDepartment = React.useRef<string>('');
|
||||
|
||||
|
||||
const [state, setState] = React.useState<IStaffDirectoryState>({
|
||||
listUsers: [],
|
||||
hasError: false,
|
||||
errorMessage: "",
|
||||
isLoading: true,
|
||||
updateUsersPresence: false,
|
||||
nextPageLink: undefined,
|
||||
isLoadingNextPage: false,
|
||||
});
|
||||
|
||||
const picker = React.useRef(null);
|
||||
_currentUser.current = props.context.pageContext.user;
|
||||
let _currentuserProperties: IUser = {} as IUser;
|
||||
let _listUsers: IUserExtended[] = [];
|
||||
|
||||
|
||||
const _interval = props.refreshInterval * 60000;
|
||||
const _updatePresenceStatus: boolean = props.updatePresenceStatus;
|
||||
useInterval( async () => {
|
||||
|
||||
_listUsers = state.listUsers;
|
||||
if (_listUsers) {
|
||||
_listUsers = await useGetUsersPresence(_listUsers, _msGraphClient.current);
|
||||
|
||||
|
||||
setState({
|
||||
...state,
|
||||
listUsers: _listUsers,
|
||||
isLoading: false,
|
||||
updateUsersPresence: true,
|
||||
});
|
||||
}
|
||||
},(!_updatePresenceStatus || isNaN(_interval) ? null : _interval ));
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
_msGraphClient.current = await props.context.msGraphClientFactory.getClient();
|
||||
_currentUser.current = props.context.pageContext.user;
|
||||
|
||||
_appContext.current.currentUser = _currentUser.current ;
|
||||
_appContext.current.msGraphClient = _msGraphClient.current;
|
||||
_appContext.current.themeVariant = props.themeVariant;
|
||||
|
||||
_currentuserProperties = await useGetUserId(
|
||||
_currentUser.current.email,
|
||||
_msGraphClient.current
|
||||
);
|
||||
setState({
|
||||
...state,
|
||||
isLoading: true,
|
||||
});
|
||||
_currentUserDepartment.current = _currentuserProperties.department;
|
||||
|
||||
const _usersResults = await useGetUsersByDepartment(
|
||||
_currentUserDepartment.current,
|
||||
_msGraphClient.current,
|
||||
props.pageSize
|
||||
);
|
||||
_listUsers = _usersResults.usersExtended;
|
||||
|
||||
setState({
|
||||
...state,
|
||||
listUsers: _listUsers,
|
||||
isLoading: false,
|
||||
updateUsersPresence: false,
|
||||
nextPageLink: _usersResults.nextPage,
|
||||
});
|
||||
// Pooling status each x min ( value define as property in webpart)
|
||||
} catch (error) {
|
||||
setState({
|
||||
...state,
|
||||
errorMessage: error.message
|
||||
? error.message
|
||||
: " Error searching users, please try later or contact support.",
|
||||
hasError: true,
|
||||
isLoading: false,
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
})();
|
||||
}, [props]);
|
||||
|
||||
// on Filter changed
|
||||
const _onFilterChanged = async (
|
||||
filterText: string,
|
||||
currentPersonas: IPersonaProps[],
|
||||
limitResults?: number
|
||||
): Promise<IPersonaProps[]> => {
|
||||
let filteredPersonas: IPersonaProps[] = [];
|
||||
|
||||
if (filterText.trim().length > 0) {
|
||||
try {
|
||||
const _usersResults = await useSearchUsers(
|
||||
"displayName:" + filterText,
|
||||
_msGraphClient.current
|
||||
);
|
||||
filteredPersonas = [];
|
||||
for (const _user of _usersResults.usersExtended) {
|
||||
filteredPersonas.push({
|
||||
text: _user.displayName,
|
||||
presence:presenceStatus[_user.availability].presenceStatus,
|
||||
presenceTitle:
|
||||
presenceStatus[_user.availability].presenceStatusLabel,
|
||||
imageUrl: _user.pictureBase64,
|
||||
secondaryText: _user.department,
|
||||
});
|
||||
}
|
||||
filteredPersonas = removeDuplicates(filteredPersonas, currentPersonas);
|
||||
return filteredPersonas;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// On Picker Changed
|
||||
const _onPickerChange = async (items: IPersonaProps[]) => {
|
||||
if (!(items.length === 0)) return;
|
||||
try {
|
||||
setState({
|
||||
...state,
|
||||
isLoading: true,
|
||||
});
|
||||
const _usersResults = await useGetUsersByDepartment(
|
||||
_currentUserDepartment.current,
|
||||
_msGraphClient.current,
|
||||
props.pageSize
|
||||
);
|
||||
setState({
|
||||
...state,
|
||||
nextPageLink: _usersResults.nextPage,
|
||||
listUsers: _usersResults.usersExtended,
|
||||
updateUsersPresence: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setState({
|
||||
...state,
|
||||
listUsers: [],
|
||||
hasError: true,
|
||||
errorMessage: error.message
|
||||
? error.message
|
||||
: " Error searching users, please try later or contact support.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _onNextPage = async (
|
||||
event: React.MouseEvent<
|
||||
HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase,
|
||||
MouseEvent
|
||||
>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
let { listUsers, nextPageLink } = state;
|
||||
|
||||
setState({
|
||||
...state,
|
||||
isLoadingNextPage: true,
|
||||
});
|
||||
try {
|
||||
const _usersResults = await useGetUsersNextPage(
|
||||
nextPageLink,
|
||||
_msGraphClient.current
|
||||
);
|
||||
const _newlistUsers = listUsers.concat(_usersResults.usersExtended);
|
||||
|
||||
setState({
|
||||
...state,
|
||||
listUsers: _newlistUsers,
|
||||
isLoadingNextPage: false,
|
||||
nextPageLink: _usersResults.nextPage,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setState({
|
||||
...state,
|
||||
listUsers: [],
|
||||
hasError: true,
|
||||
isLoadingNextPage: false,
|
||||
errorMessage: error.message
|
||||
? error.message
|
||||
: strings.ErrorMessage
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Render compoent
|
||||
// Has Error
|
||||
if (state.hasError) {
|
||||
return (
|
||||
<MessageBar messageBarType={MessageBarType.error}>{state.errorMessage}</MessageBar>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={_appContext.current}>
|
||||
<Customizer settings={{ theme: props.themeVariant }}>
|
||||
<WebPartTitle
|
||||
displayMode={props.displayMode}
|
||||
title={props.title}
|
||||
themeVariant={props.themeVariant}
|
||||
updateProperty={props.updateProperty}
|
||||
className={styleClasses.webPartTitle}
|
||||
/>
|
||||
<Stack tokens={stackTokens} style={{ width: "100%" }}>
|
||||
<Stack
|
||||
style={{ width: "100%" }}
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 0 }}
|
||||
>
|
||||
<div className={styleClasses.styleIcon}>
|
||||
<FontIcon
|
||||
iconName="Search"
|
||||
style={{
|
||||
verticalAlign: "center",
|
||||
fontSize: props.themeVariant.fonts.mediumPlus.fontSize,
|
||||
color: props.themeVariant.palette.themePrimary,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<NormalPeoplePicker
|
||||
itemLimit={1}
|
||||
onResolveSuggestions={_onFilterChanged}
|
||||
getTextFromItem={getTextFromItem}
|
||||
pickerSuggestionsProps={suggestionProps}
|
||||
className="ms-PeoplePicker"
|
||||
key="normal"
|
||||
onValidateInput={validateInput}
|
||||
onChange={_onPickerChange}
|
||||
styles={pickerStyles}
|
||||
componentRef={picker}
|
||||
onInputChange={onInputChange}
|
||||
resolveDelay={300}
|
||||
disabled={state.isLoading}
|
||||
onItemSelected={async (selectedItem: IPersonaProps) => {
|
||||
const _useSearchUsers = await useSearchUsers(
|
||||
`displayName:${selectedItem.text.trim()}`,
|
||||
_msGraphClient.current,
|
||||
props.pageSize
|
||||
);
|
||||
setState({
|
||||
...state,
|
||||
nextPageLink: _useSearchUsers.nextPage,
|
||||
listUsers: _useSearchUsers.usersExtended,
|
||||
isLoading: false,
|
||||
updateUsersPresence: false,
|
||||
});
|
||||
return selectedItem;
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{state.isLoading ? (
|
||||
<Spinner size={SpinnerSize.medium}></Spinner>
|
||||
) : (
|
||||
<div
|
||||
className={`${styleClasses.listContainer} ${styles.hideScrollBar}`}
|
||||
style={{ maxHeight: props.maxHeight }}
|
||||
>
|
||||
{state.listUsers.length > 0 ? (
|
||||
state.listUsers.map((user, i) => {
|
||||
return (
|
||||
<>
|
||||
<UserCard
|
||||
userData={user}
|
||||
userAttributes={props.userAttributes}
|
||||
updateUsersPresence={state.updateUsersPresence}
|
||||
></UserCard>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
<Stack
|
||||
horizontalAlign="center"
|
||||
verticalAlign="center"
|
||||
style={{ marginBottom: 25 }}
|
||||
>
|
||||
<Image
|
||||
src={imageNoData}
|
||||
imageFit={ImageFit.cover}
|
||||
width={250}
|
||||
height={300}
|
||||
></Image>
|
||||
<Text variant="large">No colleagues found </Text>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
{state.nextPageLink && (
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="end"
|
||||
verticalAlign="center"
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginRight: 20,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
styles={nextPageStyle}
|
||||
disabled={state.isLoadingNextPage}
|
||||
onClick={_onNextPage}
|
||||
>
|
||||
Next Page
|
||||
</Link>
|
||||
{state.isLoadingNextPage && (
|
||||
<Spinner
|
||||
style={{ marginLeft: 5 }}
|
||||
size={SpinnerSize.small}
|
||||
></Spinner>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</Customizer>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Get text from Persona
|
||||
const getTextFromItem = (persona) => {
|
||||
return persona.text;
|
||||
};
|
||||
// Remove dumplicate Items
|
||||
const removeDuplicates = (personas, possibleDupes) => {
|
||||
return personas.filter((persona) => {
|
||||
return !listContainsPersona(persona, possibleDupes);
|
||||
});
|
||||
};
|
||||
// Check if selecte list has a persona selected
|
||||
const listContainsPersona = (persona, personas) => {
|
||||
if (!personas || !personas.length || personas.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
personas.filter((item) => {
|
||||
return item.text === persona.text;
|
||||
}).length > 0
|
||||
);
|
||||
};
|
||||
// Validate Input Function
|
||||
const validateInput = (input) => {
|
||||
if (input.trim().length > 1) {
|
||||
return ValidationState.valid;
|
||||
} else {
|
||||
return ValidationState.invalid;
|
||||
}
|
||||
};
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param input The text entered into the picker.
|
||||
*/
|
||||
const onInputChange = (input) => {
|
||||
/* const outlookRegEx = /<.*>/g;
|
||||
const emailAddress = outlookRegEx.exec(input);
|
||||
|
||||
if (emailAddress && emailAddress[0]) {
|
||||
return emailAddress[0].substring(1, emailAddress[0].length - 1);
|
||||
} */
|
||||
return input;
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { IUserExtended } from "../../entites/IUserExtended";
|
||||
export interface IUserCardProps {
|
||||
userData: IUserExtended;
|
||||
updateUsersPresence:boolean;
|
||||
userAttributes: string[];
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
@media (min-width: 480px) {
|
||||
.media
|
||||
{
|
||||
min-width: 100%!important;
|
||||
}
|
||||
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.media
|
||||
{
|
||||
min-width: 47%!important;
|
||||
}
|
||||
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.media
|
||||
{
|
||||
min-width: 32% !important;
|
||||
}
|
||||
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.media
|
||||
{
|
||||
min-width: 22% !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.media {
|
||||
min-width: 290px;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
|
||||
|
||||
.hideScrollBar::-webkit-scrollbar{
|
||||
display:none
|
||||
}
|
||||
|
||||
:global {
|
||||
.media {
|
||||
width: '100%';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.media
|
||||
{
|
||||
width: '100%';
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.media
|
||||
{
|
||||
width: '47%';
|
||||
}
|
||||
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.media
|
||||
{
|
||||
width: '32%';
|
||||
}
|
||||
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.media
|
||||
{
|
||||
width: '22%';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,710 @@
|
|||
import * as React from "react";
|
||||
import {
|
||||
mergeStyleSets,
|
||||
Persona,
|
||||
PersonaSize,
|
||||
Stack,
|
||||
Label,
|
||||
FontIcon,
|
||||
Text,
|
||||
Link,
|
||||
ITextFieldStyles,
|
||||
IPersonaProps,
|
||||
} from "office-ui-fabric-react";
|
||||
import { presenceStatus, IPresenceStatus } from "../../common/PresenceStatus";
|
||||
import { AppContext } from "../../common/AppContext";
|
||||
import { ActionButton } from "office-ui-fabric-react";
|
||||
import { IUserExtended } from "../../entites/IUserExtended";
|
||||
import { IAppContext } from "../../common/IAppContext";
|
||||
import { IUserCardProps } from "./IUserCardProps";
|
||||
|
||||
export const UserCard = (props: IUserCardProps) => {
|
||||
const { userData, updateUsersPresence } = props;
|
||||
const _context: IAppContext = React.useContext(AppContext);
|
||||
|
||||
const [expandIcon, setExpandIcon] = React.useState("ChevronDownSmall");
|
||||
const [isDetailsOpen, setIsDetailsOpen] = React.useState(false);
|
||||
|
||||
|
||||
const styleClasses = mergeStyleSets({
|
||||
separator: {
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
marginTop: 15,
|
||||
borderBottomStyle: "solid",
|
||||
borderWidth: 1,
|
||||
borderBottomColor: _context.themeVariant.palette.neutralLighterAlt,
|
||||
},
|
||||
stylContainerDetails: {
|
||||
marginTop: 25,
|
||||
display: "grid",
|
||||
justifyContent: "stretch",
|
||||
alignItems: "center",
|
||||
backgroundColor: _context.themeVariant.palette.themeLighter,
|
||||
gridTemplateColumns: "repeat( auto-fit, minmax(280px, 1fr) )",
|
||||
gridTemplateRows: "auto",
|
||||
},
|
||||
|
||||
styleIconDetails: {
|
||||
fontSize: 16,
|
||||
color: _context.themeVariant.palette.themePrimary,
|
||||
},
|
||||
styleFieldLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
paddingLeft: 3,
|
||||
},
|
||||
|
||||
styleField: {
|
||||
paddingTop: 15,
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
const styleTextField: Partial<ITextFieldStyles> = {
|
||||
root: {
|
||||
fontWeight: 500,
|
||||
width: "calc(100% - 46)",
|
||||
marginLeft: 45,
|
||||
paddingRight: 20,
|
||||
},
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (!updateUsersPresence) {
|
||||
setExpandIcon("ChevronDownSmall");
|
||||
setIsDetailsOpen(false);
|
||||
}
|
||||
})();
|
||||
}, [userData]);
|
||||
|
||||
const _onShowDetails = (event) => {
|
||||
event.preventDefault();
|
||||
if (!isDetailsOpen) {
|
||||
setExpandIcon("ChevronUpSmall");
|
||||
setIsDetailsOpen(true);
|
||||
} else {
|
||||
setExpandIcon("ChevronDownSmall");
|
||||
setIsDetailsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _onRenderPrimaryText = (persona: IPersonaProps) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="start"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
styles={{
|
||||
root: { justifyContent: "flex-start", width: "100%" },
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="medium"
|
||||
block
|
||||
nowrap
|
||||
style={{
|
||||
width: "100%",
|
||||
fontWeight: 600,
|
||||
padding: 0,
|
||||
marginBottom: 3,
|
||||
}}
|
||||
>
|
||||
{persona.text}
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal
|
||||
horizontalAlign="end"
|
||||
verticalAlign="start"
|
||||
tokens={{ childrenGap: 3 }}
|
||||
style={{ width: "100%", maxHeight: 21 }}
|
||||
>
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<ActionButton
|
||||
styles={{
|
||||
root: {
|
||||
height: 21,
|
||||
width: 26,
|
||||
color: _context.themeVariant.palette.themePrimary,
|
||||
},
|
||||
}}
|
||||
iconProps={{
|
||||
iconName: "CannedChat",
|
||||
color: _context.themeVariant.palette.themePrimary,
|
||||
}}
|
||||
allowDisabledFocus={true}
|
||||
disabled={false}
|
||||
checked={true}
|
||||
title="Start Teams Chat"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
window.open(
|
||||
"https://teams.microsoft.com/l/chat/0/0?users=" +
|
||||
userData.mail +
|
||||
"&message=Hi " +
|
||||
userData.displayName,
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
></ActionButton>
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
styles={{
|
||||
root: {
|
||||
height: 21,
|
||||
width: 26,
|
||||
color: _context.themeVariant.palette.themePrimary,
|
||||
},
|
||||
}}
|
||||
iconProps={{
|
||||
iconName: expandIcon,
|
||||
color: _context.themeVariant.palette.themePrimary,
|
||||
}}
|
||||
allowDisabledFocus={true}
|
||||
disabled={false}
|
||||
checked={true}
|
||||
title={isDetailsOpen ? "Hide details" : "Show Details"}
|
||||
onClick={_onShowDetails}
|
||||
></ActionButton>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const _onRenderSecondaryText = (persona: IPersonaProps) => {
|
||||
return (
|
||||
<>
|
||||
<Stack verticalAlign="start" tokens={{ childrenGap: 0 }}>
|
||||
<Text title={persona.secondaryText} variant="medium" block nowrap>
|
||||
{" "}
|
||||
{persona.secondaryText}
|
||||
</Text>
|
||||
<Text
|
||||
title={presenceStatus[userData.availability].presenceStatusLabel}
|
||||
variant="smallPlus"
|
||||
block
|
||||
nowrap
|
||||
>
|
||||
{presenceStatus[userData.availability].presenceStatusLabel}
|
||||
</Text>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 20 }}
|
||||
style={{ padding: 10 }}
|
||||
>
|
||||
<Persona
|
||||
size={PersonaSize.size48}
|
||||
imageUrl={userData.pictureBase64}
|
||||
presence={presenceStatus[userData.availability].presenceStatus}
|
||||
presenceTitle={
|
||||
presenceStatus[userData.availability].presenceStatusLabel
|
||||
}
|
||||
text={userData.displayName}
|
||||
title={userData.displayName}
|
||||
tertiaryText={userData.mail}
|
||||
secondaryText={userData.jobTitle}
|
||||
onRenderPrimaryText={_onRenderPrimaryText}
|
||||
onRenderSecondaryText={_onRenderSecondaryText}
|
||||
></Persona>
|
||||
</Stack>
|
||||
{isDetailsOpen && (
|
||||
<>
|
||||
<Stack
|
||||
style={{ width: "100%" }}
|
||||
className={styleClasses.stylContainerDetails}
|
||||
>
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={
|
||||
userData.department ? userData.department : "Not available"
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="Teamwork"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Department
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.department ? userData.department : "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={
|
||||
userData.businessPhones.join(",")
|
||||
? userData.businessPhones.join(",")
|
||||
: "Not available"
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="Phone"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Business Phones
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.businessPhones.join(",") ? (
|
||||
<>
|
||||
<Link href={`CALLTO:${userData.businessPhones[0]}`}>
|
||||
{userData.businessPhones[0]}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
"Not available"
|
||||
)}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={userData.mail ? userData.mail : "Not Available"}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="Mail"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>E-mail</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.mail ? (
|
||||
<>
|
||||
<Link href={`MAILTO:${userData.mail}`}>
|
||||
{userData.mail}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
"Not available"
|
||||
)}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
|
||||
{props.userAttributes.length > 0 &&
|
||||
props.userAttributes.map((attribute, i) => {
|
||||
switch (attribute) {
|
||||
case "mobilePhone":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={
|
||||
userData.mobilePhone
|
||||
? userData.mobilePhone
|
||||
: "Not available"
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="CellPhone"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Mobile Phone
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.mobilePhone ? (
|
||||
<>
|
||||
<Link href={`CALLTO:${userData.mobilePhone}`}>
|
||||
{userData.mobilePhone}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
"Not available"
|
||||
)}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
break;
|
||||
case "company":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={userData.companyName ? userData.companyName : "Not available"}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="TextField"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Company Name
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.companyName ? userData.companyName : "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "officeLocation":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={
|
||||
userData.officeLocation
|
||||
? userData.officeLocation
|
||||
: "Not available"
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="POISolid"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Office Location
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.officeLocation
|
||||
? userData.officeLocation
|
||||
: "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "city":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={userData.city ? userData.city : "Not available"}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="CityNext2"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
City
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.city ? userData.city : "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "postalCode":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={userData.postalCode ? userData.postalCode : "Not available"}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="NumberField"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Postal Code
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{ userData.postalCode ? userData.postalCode : "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "country":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={
|
||||
userData.country ? userData.country : "Not available"
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="World"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Country
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.country
|
||||
? userData.country
|
||||
: "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "userType":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={
|
||||
userData.userType
|
||||
? userData.userType
|
||||
: "Not available"
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="UserOptional"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Office 365 User Type
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.userType
|
||||
? userData.userType
|
||||
: "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "employeeId":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={userData.employeeId ? userData.employeeId : "Not available"}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="Contact"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
Employee Id
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.employeeId ? userData.employeeId : "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "imAddresses":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={
|
||||
userData.imAddresses.join(",")
|
||||
? userData.imAddresses.join(",")
|
||||
: "Not available"
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="CannedChat"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
IMS Address
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.imAddresses.join(",") ? (
|
||||
<>
|
||||
<Link
|
||||
data-interception="off"
|
||||
target="_blank"
|
||||
href={`https://teams.microsoft.com/l/chat/0/0?users=${userData.imAddresses[0]}`}
|
||||
>
|
||||
{userData.imAddresses[0]}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
"Not available"
|
||||
)}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "userType":
|
||||
return (
|
||||
<div
|
||||
className={`${styleClasses.styleField}`}
|
||||
title={userData.userType ? userData.userType : "Not available"}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
tokens={{ childrenGap: 5 }}
|
||||
style={{ marginRight: 20, marginLeft: 20 }}
|
||||
>
|
||||
<FontIcon
|
||||
className={styleClasses.styleIconDetails}
|
||||
iconName="CityNext2"
|
||||
/>
|
||||
<Label className={styleClasses.styleFieldLabel}>
|
||||
User Type
|
||||
</Label>
|
||||
</Stack>
|
||||
<Text
|
||||
styles={styleTextField}
|
||||
variant="medium"
|
||||
block={true}
|
||||
nowrap={true}
|
||||
>
|
||||
{userData.userType ? userData.userType : "Not available"}
|
||||
</Text>
|
||||
<div className={styleClasses.separator}></div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
export interface IUser {
|
||||
id?: string;
|
||||
displayName: string;
|
||||
mail: string;
|
||||
jobTitle: string;
|
||||
mobilePhone: string;
|
||||
department: string;
|
||||
businessPhones: string[];
|
||||
userPrincipalName: string;
|
||||
city: string;
|
||||
companyName: string;
|
||||
country: string;
|
||||
employeeId: string;
|
||||
imAddresses: string[];
|
||||
officeLocation: string;
|
||||
postalCode: string;
|
||||
userType: string;
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IUser } from "./IUser";
|
||||
import { IUserPresence } from "./IUserPresence";
|
||||
export interface IUserExtended extends IUser, IUserPresence {
|
||||
count: number;
|
||||
pictureBase64: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface IUserPresence {
|
||||
"@odata.context"?: string;
|
||||
id?: string;
|
||||
availability: string;
|
||||
activity: string;
|
||||
}
|
||||
//# sourceMappingURL=IUserPresence.d.ts.map
|
|
@ -0,0 +1,22 @@
|
|||
import { useRef } from "react";
|
||||
import React from "react";
|
||||
|
||||
export const useInterval = (callback:any, delay:number) => {
|
||||
const savedCallback = useRef(null);
|
||||
|
||||
// Remember the latest function.
|
||||
React.useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
React.useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current();
|
||||
}
|
||||
if (delay !== null) {
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
};
|
|
@ -0,0 +1,281 @@
|
|||
import "@pnp/sp/profiles";
|
||||
import { MSGraphClient } from "@microsoft/sp-http";
|
||||
import "@pnp/graph/users";
|
||||
import { IUserExtended } from "../entites/IUserExtended";
|
||||
import { IUser } from "../entites/IUser";
|
||||
import { IUserPresence } from "../entites/IUserPresence";
|
||||
import { SPComponentLoader } from "@microsoft/sp-loader";
|
||||
import { findIndex } from "lodash";
|
||||
|
||||
/*************************************************************************************/
|
||||
// Hook to search users
|
||||
//*************************************************************************************/
|
||||
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=";
|
||||
|
||||
// Search Users
|
||||
export const useSearchUsers = async (
|
||||
searchString: string,
|
||||
_MSGraphClient: MSGraphClient,
|
||||
pageSize?: number
|
||||
): Promise<{ usersExtended: IUserExtended[]; nextPage: string }> => {
|
||||
pageSize = pageSize ? pageSize : 5;
|
||||
const _searchResults: any = await _MSGraphClient
|
||||
.api('/users?$search="' + searchString + '"')
|
||||
.version("beta")
|
||||
.header("ConsistencyLevel", "eventual")
|
||||
.select(
|
||||
"id,displayName,jobTitle,mail,mobilePhone,department,businessPhones,userPrincipalName,city,companyName,country,employeeId,imAddresses,officeLocation,postalCode,userType"
|
||||
)
|
||||
.top(pageSize)
|
||||
.orderby("displayName")
|
||||
.count(true)
|
||||
.get();
|
||||
|
||||
const _users: IUser[] = _searchResults.value;
|
||||
let _usersExtended: IUserExtended[] = [];
|
||||
|
||||
for (const _user of _users) {
|
||||
const _userPresence = await getUserPresence(_user.id, _MSGraphClient);
|
||||
const _pictureBase64: string = await getUserPhoto(_user.mail);
|
||||
_usersExtended.push({
|
||||
..._user,
|
||||
..._userPresence,
|
||||
pictureBase64: _pictureBase64,
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
let _nextPage: string = undefined;
|
||||
if (_searchResults["@odata.nextLink"]) {
|
||||
_nextPage = _searchResults["@odata.nextLink"];
|
||||
}
|
||||
return { usersExtended: _usersExtended, nextPage: _nextPage };
|
||||
};
|
||||
|
||||
// Get Users by department
|
||||
export const useGetUsersByDepartment = async (
|
||||
filter: string,
|
||||
_MSGraphClient: MSGraphClient,
|
||||
pageSize?: number
|
||||
): Promise<{ usersExtended: IUserExtended[]; nextPage: string }> => {
|
||||
pageSize = pageSize ? pageSize : 5;
|
||||
const _filter: string =
|
||||
filter && filter.trim().length > 0
|
||||
? `?$filter=department eq '${encodeURIComponent(filter)}'`
|
||||
: ""; // if department is blanks get first 1000 users
|
||||
const _searchResults: any = await _MSGraphClient
|
||||
.api(`/users${_filter}`)
|
||||
.version("beta")
|
||||
.header("ConsistencyLevel", "eventual")
|
||||
.select(
|
||||
"id,displayName,jobTitle,mail,mobilePhone,department,businessPhones,userPrincipalName,city,companyName,country,employeeId,imAddresses,officeLocation,postalCode,userType"
|
||||
)
|
||||
.top(pageSize)
|
||||
.orderby("displayName")
|
||||
.count(true)
|
||||
.get();
|
||||
|
||||
const _users: IUser[] = _searchResults.value;
|
||||
let _usersExtended: IUserExtended[] = [];
|
||||
|
||||
for (const _user of _users) {
|
||||
const _userPresence = await getUserPresence(_user.id, _MSGraphClient);
|
||||
const _pictureBase64: string = await getUserPhoto(_user.mail);
|
||||
_usersExtended.push({
|
||||
..._user,
|
||||
..._userPresence,
|
||||
pictureBase64: _pictureBase64,
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
let _nextPage: string = undefined;
|
||||
if (_searchResults["@odata.nextLink"]) {
|
||||
_nextPage = _searchResults["@odata.nextLink"];
|
||||
}
|
||||
return { usersExtended: _usersExtended, nextPage: _nextPage };
|
||||
};
|
||||
|
||||
// Get Users Next Page
|
||||
export const useGetUsersNextPage = async (
|
||||
nextPageLink: string,
|
||||
_MSGraphClient: MSGraphClient
|
||||
): Promise<{ usersExtended: IUserExtended[]; nextPage: string }> => {
|
||||
const _searchResults: any = await _MSGraphClient
|
||||
.api(`${nextPageLink}`)
|
||||
.version("beta")
|
||||
.header("ConsistencyLevel", "eventual")
|
||||
.select(
|
||||
"id,displayName,jobTitle,mail,mobilePhone,department,businessPhones,userPrincipalName,city,companyName,country,employeeId,imAddresses,officeLocation,postalCode,userType"
|
||||
)
|
||||
.orderby("displayName")
|
||||
.count(true)
|
||||
.get();
|
||||
|
||||
const _users: IUser[] = _searchResults.value;
|
||||
let _usersExtended: IUserExtended[] = [];
|
||||
|
||||
for (const _user of _users) {
|
||||
const _userPresence = await getUserPresence(_user.id, _MSGraphClient);
|
||||
const _pictureBase64: string = await getUserPhoto(_user.mail);
|
||||
_usersExtended.push({
|
||||
..._user,
|
||||
..._userPresence,
|
||||
pictureBase64: _pictureBase64,
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
let _nextPage: string = undefined;
|
||||
if (_searchResults["@odata.nextLink"]) {
|
||||
_nextPage = _searchResults["@odata.nextLink"];
|
||||
}
|
||||
return { usersExtended: _usersExtended, nextPage: _nextPage };
|
||||
};
|
||||
|
||||
//*************************************************************************************//
|
||||
// function Get Users Presence
|
||||
//*************************************************************************************//
|
||||
export const useGetUsersPresence = async (
|
||||
users: IUserExtended[],
|
||||
_MSGraphClient: MSGraphClient
|
||||
): Promise<IUserExtended[]> => {
|
||||
let userObjsIds: string[] = [];
|
||||
|
||||
// debugger;
|
||||
|
||||
for (const _user of users) {
|
||||
userObjsIds.push(_user.id);
|
||||
}
|
||||
|
||||
if (userObjsIds.length > 0) {
|
||||
const _presences: any = await _MSGraphClient
|
||||
.api(`/communications/getPresencesByUserId`)
|
||||
.version("beta")
|
||||
.post({ ids: userObjsIds });
|
||||
|
||||
// update presence
|
||||
for (const _presence of _presences.value) {
|
||||
const i = findIndex(users, (v) => {
|
||||
return v.id == _presence.id;
|
||||
});
|
||||
users[i] = {
|
||||
...users[i],
|
||||
..._presence,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
//*************************************************************************************//
|
||||
// function Get Users Presence
|
||||
//*************************************************************************************//
|
||||
|
||||
const getUserPresence = async (
|
||||
userObjId,
|
||||
_MSGraphClient
|
||||
): Promise<IUserPresence> => {
|
||||
let _presence: IUserPresence = await _MSGraphClient
|
||||
.api("/users/" + userObjId + "/presence")
|
||||
.version("beta")
|
||||
.get();
|
||||
|
||||
return _presence;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets user photo
|
||||
* @param userId
|
||||
* @returns user photo
|
||||
*/
|
||||
const getUserPhoto = async (userId: string): Promise<string> => {
|
||||
const personaImgUrl = PROFILE_IMAGE_URL + userId;
|
||||
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 if user has default image or custom image
|
||||
* @param url
|
||||
*/
|
||||
const getMd5HashForUrl = (url: string) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const library: any = await loadSPComponentById(MD5_MODULE_ID);
|
||||
try {
|
||||
const md5Hash = library.Md5Hash;
|
||||
if (md5Hash) {
|
||||
const convertedHash = md5Hash(url);
|
||||
resolve(convertedHash);
|
||||
}
|
||||
} catch (error) {
|
||||
resolve(url);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Load SPFx component by id, SPComponentLoader is used to load the SPFx components
|
||||
* @param componentId - componentId, guid of the component library
|
||||
*/
|
||||
const loadSPComponentById = (componentId: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
SPComponentLoader.loadComponentById(componentId)
|
||||
.then((component: any) => {
|
||||
resolve(component);
|
||||
})
|
||||
.catch((error) => {});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets image base64
|
||||
* @param pictureUrl
|
||||
* @returns image base64
|
||||
*/
|
||||
const getImageBase64 = async (pictureUrl: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let image = new Image();
|
||||
image.addEventListener("load", () => {
|
||||
let 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;
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserId = async (user, _MSGraphClient): Promise<IUser> => {
|
||||
const _usersResults: IUser = await _MSGraphClient
|
||||
.api("/users/" + user)
|
||||
.version("beta")
|
||||
.header("ConsistencyLevel", "eventual")
|
||||
.select(
|
||||
"id,displayName,jobTitle,mail,mobilePhone,department,businessPhones,userPrincipalName,city,companyName,country,employeeId,imAddresses,officeLocation,postalCode,userType"
|
||||
)
|
||||
.orderby("displayName")
|
||||
.count(true)
|
||||
.get();
|
||||
|
||||
// Get UserID
|
||||
return _usersResults;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "587a52af-e713-42a1-86b9-04ae167d4954",
|
||||
"alias": "StaffDirectoryWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"supportsThemeVariants": true,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart"],
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "SPFx Custom Web Parts" },
|
||||
"title": { "default": "Search Directory" },
|
||||
"description": { "default": "Search Directory" },
|
||||
"officeFabricIconFontName": "ProfileSearch",
|
||||
"properties": {
|
||||
"title": "Search Directory",
|
||||
"maxHeight": 700,
|
||||
"showBox": true,
|
||||
"refreshInterval": 3,
|
||||
"updatePresenceStatus": true,
|
||||
"userAttributes": [],
|
||||
"pageSize": 5
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version, DisplayMode } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneSlider,
|
||||
PropertyPaneToggle,
|
||||
PropertyPaneHorizontalRule
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart, PropertyPaneLabel } from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'StaffDirectoryWebPartStrings';
|
||||
import {StaffDirectory} from '../../components/StaffDirectory/StaffDirectory';
|
||||
import { IStaffDirectoryProps } from '../../components/StaffDirectory/IStaffDirectoryProps';
|
||||
import { PropertyFieldMultiSelect } from '@pnp/spfx-property-controls/lib/PropertyFieldMultiSelect';
|
||||
|
||||
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
export interface IStaffDirectoryWebPartProps {
|
||||
title: string;
|
||||
maxHeight: number;
|
||||
showBox: boolean;
|
||||
displayMode: DisplayMode;
|
||||
refreshInterval: number;
|
||||
updateProperty: (value: string) => void;
|
||||
updatePresenceStatus: boolean;
|
||||
userAttributes: string[];
|
||||
pageSize:number;
|
||||
}
|
||||
|
||||
export default class StaffDirectoryWebPart extends BaseClientSideWebPart<IStaffDirectoryWebPartProps> {
|
||||
|
||||
private _themeProvider: ThemeProvider;
|
||||
private _themeVariant: IReadonlyTheme | undefined;
|
||||
protected async onInit(): Promise<void> {
|
||||
window.sessionStorage.clear();
|
||||
this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
|
||||
// If it exists, get the theme variant
|
||||
this._themeVariant = this._themeProvider.tryGetTheme();
|
||||
|
||||
// Register a handler to be notified if the theme variant changes
|
||||
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
|
||||
this._themeVariant = args.theme;
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IStaffDirectoryProps> = React.createElement(
|
||||
StaffDirectory,
|
||||
{
|
||||
title: this.properties.title,
|
||||
context: this.context,
|
||||
maxHeight: this.properties.maxHeight,
|
||||
showBox: this.properties.showBox,
|
||||
themeVariant: this._themeVariant,
|
||||
displayMode: this.displayMode,
|
||||
updateProperty: (value:string ) => {
|
||||
this.properties.title = value;
|
||||
},
|
||||
refreshInterval: this.properties.refreshInterval,
|
||||
updatePresenceStatus: this.properties.updatePresenceStatus,
|
||||
userAttributes: this.properties.userAttributes,
|
||||
pageSize: this.properties.pageSize,
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
value: this.properties.title
|
||||
}),
|
||||
PropertyPaneHorizontalRule(),
|
||||
PropertyPaneSlider("maxHeight",{
|
||||
min: 280,
|
||||
max: 1200,
|
||||
value: this.properties.maxHeight,
|
||||
label: strings.MaxHeightLabel
|
||||
}),
|
||||
PropertyPaneHorizontalRule(),
|
||||
PropertyPaneToggle("showBox",{
|
||||
label: strings.ShowBoxLabel,
|
||||
key:"showBox",
|
||||
checked: this.properties.showBox
|
||||
}),
|
||||
PropertyPaneToggle("updatePresenceStatus",{
|
||||
label: strings.UpdatePresenceStatusLabel,
|
||||
key:"presenceStatus",
|
||||
checked: this.properties.updatePresenceStatus
|
||||
}),
|
||||
|
||||
PropertyPaneHorizontalRule(),
|
||||
|
||||
this.properties.updatePresenceStatus && (
|
||||
PropertyPaneSlider("refreshInterval",{
|
||||
min: 3,
|
||||
max: 15,
|
||||
value: this.properties.refreshInterval,
|
||||
label: strings.RefreshIntervalLabel
|
||||
})
|
||||
),
|
||||
PropertyPaneLabel('',{text:''}),
|
||||
PropertyPaneHorizontalRule(),
|
||||
|
||||
PropertyFieldMultiSelect('userAttributes', {
|
||||
key: 'userAttributes',
|
||||
label: strings.UserAttributesLabel,
|
||||
|
||||
options: [
|
||||
{
|
||||
key: "company",
|
||||
text: "Company"
|
||||
},
|
||||
{
|
||||
key: "officeLocation",
|
||||
text: "Office Location"
|
||||
},
|
||||
{
|
||||
key: "mobilePhone",
|
||||
text: "Mobile Phone",
|
||||
},
|
||||
{
|
||||
key: "postalCode",
|
||||
text: "Postal Code"
|
||||
},
|
||||
{
|
||||
key: "country",
|
||||
text: "Country"
|
||||
},
|
||||
{
|
||||
key: "employeeId",
|
||||
text: "Employee Id"
|
||||
},
|
||||
{
|
||||
key: "imAddresses",
|
||||
text: "IMS Address"
|
||||
},
|
||||
{
|
||||
key: "userType",
|
||||
text: "User Type"
|
||||
},
|
||||
],
|
||||
selectedKeys: this.properties.userAttributes
|
||||
}),
|
||||
PropertyPaneLabel('',{text:''}),
|
||||
PropertyPaneHorizontalRule(),
|
||||
PropertyPaneSlider("pageSize",{
|
||||
min: 5,
|
||||
max: 100,
|
||||
value: this.properties.pageSize,
|
||||
label: strings.PageSizeLabel
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
define([], function() {
|
||||
return {
|
||||
ErrorMessage: " Error searching users, please try later or contact support.",
|
||||
UserAttributesLabel: "Select aditional user properties to show",
|
||||
"PropertyPaneDescription": "Search for user in Azure AD Active Directory, Initial shows colleagues of same department",
|
||||
"BasicGroupName": "Properties",
|
||||
"TitleFieldLabel": "Title",
|
||||
"MaxHeightLabel": "Web Part max height",
|
||||
"ShowBoxLabel": "Show Backgroud Box",
|
||||
"RefreshIntervalLabel": "Refresh Presence Status (min)",
|
||||
"UpdatePresenceStatusLabel": "Update Presence Status?",
|
||||
"PageSizeLabel": "Number of users to show per page",
|
||||
}
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
declare interface IStaffDirectoryWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
ErrorMessage: string;
|
||||
TitleFieldLabel: string;
|
||||
MaxHeightLabel:string;
|
||||
ShowBoxLabel: string;
|
||||
RefreshIntervalLabel:string;
|
||||
UpdatePresenceStatusLabel: string;
|
||||
PageSizeLabel:string;
|
||||
UserAttributesLabel: string;
|
||||
}
|
||||
|
||||
declare module 'StaffDirectoryWebPartStrings' {
|
||||
const strings: IStaffDirectoryWebPartStrings;
|
||||
export = strings;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 383 B |
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* This script updates the package-solution version analogue to the
|
||||
* the package.json file.
|
||||
*/
|
||||
|
||||
if (process.env.npm_package_version === undefined) {
|
||||
|
||||
throw 'Package version cannot be evaluated';
|
||||
|
||||
}
|
||||
|
||||
// define path to package-solution file
|
||||
const solution = './config/package-solution.json',
|
||||
teams = './teams/manifest.json';
|
||||
|
||||
// require filesystem instance
|
||||
const fs = require('fs');
|
||||
|
||||
// get next automated package version from process variable
|
||||
const nextPkgVersion = process.env.npm_package_version;
|
||||
|
||||
// make sure next build version match
|
||||
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
|
||||
nextPkgVersion : nextPkgVersion.split('-')[0];
|
||||
|
||||
// Update version in SPFx package-solution if exists
|
||||
if (fs.existsSync(solution)) {
|
||||
|
||||
// read package-solution file
|
||||
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
|
||||
// parse file as json
|
||||
const solutionContents = JSON.parse(solutionFileContent);
|
||||
|
||||
// set property of version to next version
|
||||
solutionContents.solution.version = nextVersion + '.0';
|
||||
|
||||
// save file
|
||||
fs.writeFileSync(
|
||||
solution,
|
||||
// convert file back to proper json
|
||||
JSON.stringify(solutionContents, null, 2),
|
||||
'UTF-8');
|
||||
|
||||
}
|
||||
|
||||
// Update version in teams manifest if exists
|
||||
if (fs.existsSync(teams)) {
|
||||
|
||||
// read package-solution file
|
||||
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
|
||||
// parse file as json
|
||||
const teamsContent = JSON.parse(teamsManifestContent);
|
||||
|
||||
// set property of version to next version
|
||||
teamsContent.version = nextVersion;
|
||||
|
||||
// save file
|
||||
fs.writeFileSync(
|
||||
teams,
|
||||
// convert file back to proper json
|
||||
JSON.stringify(teamsContent, null, 2),
|
||||
'UTF-8');
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": false,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue