Merge pull request #1502 from sudharsank/react-photo-sync
This commit is contained in:
commit
eea9f0c3fd
|
@ -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,111 @@
|
|||
{
|
||||
"title": "PhotoSync",
|
||||
"steps": [
|
||||
{
|
||||
"file": "src/webparts/photoSync/PhotoSyncWebPart.ts",
|
||||
"line": 224,
|
||||
"description": "Check for current logged-in user. If the user is site addministrator, load the admin properties and if the user is a normal user, load the end user properties."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/PhotoSyncWebPart.ts",
|
||||
"line": 115,
|
||||
"description": "Admin Webpart properties"
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/PhotoSyncWebPart.ts",
|
||||
"line": 94,
|
||||
"description": "Normal user webpart properties."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/PhotoSyncWebPart.ts",
|
||||
"line": 42,
|
||||
"description": "OnInit method to do the following\n1. PnP setup\n2. Fetch graph client\n3. Initialize the custom helper class"
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/common/AppContext.ts",
|
||||
"line": 1,
|
||||
"description": "AppContext file for using React Context API. All the properties declared here will be accessible by all the child components. No need to pass the properties from parent to child components.",
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 1,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 17,
|
||||
"character": 75
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/components/PhotoSync.tsx",
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 53,
|
||||
"character": 5
|
||||
},
|
||||
"end": {
|
||||
"line": 62,
|
||||
"character": 7
|
||||
}
|
||||
},
|
||||
"description": "Assigning all the AppContext properties in the main component."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/components/PhotoSync.tsx",
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 154,
|
||||
"character": 9
|
||||
},
|
||||
"end": {
|
||||
"line": 154,
|
||||
"character": 54
|
||||
}
|
||||
},
|
||||
"description": "Using React Context API in the parent component and assigning the **value** to the appcontext properties."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/components/PhotoSync.tsx",
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 176,
|
||||
"character": 77
|
||||
},
|
||||
"end": {
|
||||
"line": 196,
|
||||
"character": 78
|
||||
}
|
||||
},
|
||||
"description": "**Pivot Menu** and the child components\n1. **UserSelectionSync** - Provide an option to select the individual users for updating their photos.\n2. **BulkPhotoSync** - Allow the users to drag and drop the photos from their fileshare to update.\n3. **SyncJobs** - Lists all the sync jobs triggered by all the users with the status and updated user details."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/common/helper.ts",
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 3,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 15,
|
||||
"character": 26
|
||||
}
|
||||
},
|
||||
"description": "Selective imports to the PnP modules."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/common/helper.ts",
|
||||
"description": "Method to retrieve the different thumbnails using MSGraph API. Since there will be multiple requests per users, requests are sent as a **batch** based on the batch limit and the number of users selected for update."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/common/helper.ts",
|
||||
"line": 251,
|
||||
"description": "Method to retrieve the different thumbnails using **MSGraph API**. Since there will be multiple requests per users, requests are sent as a **batch** based on the batch limit and the number of users selected for update."
|
||||
},
|
||||
{
|
||||
"file": "src/webparts/photoSync/common/helper.ts",
|
||||
"line": 384,
|
||||
"description": "Method to generate different thumbnails for the **BulkSync** option. Used **image-resize** npm package."
|
||||
}
|
||||
],
|
||||
"ref": "master"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.11.0",
|
||||
"libraryName": "react-photo-sync",
|
||||
"libraryId": "1feb523c-4edf-4b85-a8d3-1458ab23fae9",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
# SPUPS Photo Sync
|
||||
|
||||
## Summary
|
||||
|
||||
This web part will help the administrators to synchronize the **User Profile Photos** from **Azure AD** or from the **local filesystem** to **SharePoint User Profile Store**.
|
||||
|
||||
## Features
|
||||
|
||||
* **User selection** will help you to update only specific user based on the selection. It will also allow the users to fetch the photos from **Azure AD** before starting the synchronization.
|
||||
|
||||
* **Bulk Sync** will allow the admin to upload the photos from their file shares. The filename should be in the format '**UserID.jpg**'
|
||||
|
||||
* **Access control** based on **SharePoint Group**, not all the users can access the application.
|
||||
|
||||
* Separate section to check the **status** of the photo update.
|
||||
|
||||
* **Azure Function** to handle the photo update. **PnPPowershell** is used in Azure Function.
|
||||
|
||||
* The application supports **SPA**.
|
||||
|
||||
> **_Note_**: All the supporting lists were created when the web part is loaded for the first time. Whenever the web part is loaded, the supported lists were checked whether it exists or not.
|
||||
|
||||
## Properties
|
||||
|
||||
1. **Select a library to store the thumbnails**: A document library to store the thumbnail photos.
|
||||
|
||||
2. **Delete thumbnail stored**: This flag will decide whether you want to keep the thumbnails generated or to clean it after the sync completed.
|
||||
|
||||
3. **Azure Function URL**: Azure function URL to run the photo update.
|
||||
|
||||
4. **Use Certificate for Azure Function authentication**: The video mentioned below to setup Azure Function has different options. This setting will decide whether to use the certificate or stored credentials to communicate with SharePoint.
|
||||
|
||||
5. **Date format**: Date format to be used across the entire application. Used _**momentJS**_.
|
||||
|
||||
6. **SharePoint Groups**: Only the users from the configured SharePoint Groups and Site Administrator shall be allowed access.
|
||||
|
||||
7. **Use page full width**: This is used when the web part is added to a site page where it has to use full width.
|
||||
|
||||
## Preview
|
||||
|
||||
### User Selection sync
|
||||
|
||||
![SPUPS-Photo-Sync_1](./assets/SPUPS_Photo_Sync_1.gif)
|
||||
|
||||
### Bulk sync
|
||||
|
||||
![SPUPS-Photo-Sync_2](./assets/SPUPS_Photo_Sync_2.gif)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![SPFx 1.11](https://img.shields.io/badge/version-1.11.0-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https:/dev.office.com/sharepoint)
|
||||
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
> **@microsoft/generator-sharepoint - 1.11.0**
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
SPUPS Photo Sync | Sudharsan K.([@sudharsank](https://twitter.com/sudharsank), [SPKnowledge](https://spknowledge.com/))
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0.0|Sep 13 2020|Initial release
|
||||
|
||||
## Disclaimer
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- in the command line run:
|
||||
- `npm install`
|
||||
- `gulp bundle --ship && gulp package-solution --ship`
|
||||
- Add the .sppkg file to the app catalog and add the **Page Comments** web part to the page.
|
||||
- **Azure Function** has to be setup for property update. **The actual powershell is uploaded in the assets folder**. Follow the steps explained in the video by [Paolo Pialorsi](https://www.youtube.com/watch?v=plS_1BsQAto&list=PL-KKED6SsFo8TxDgQmvMO308p51AO1zln&index=2&t=0s).
|
||||
|
||||
|
||||
#### Local Mode
|
||||
This solution doesn't work on local mode.
|
||||
|
||||
#### SharePoint Mode
|
||||
If you want to try on a real environment, open:
|
||||
[O365 Workbench](https://your-domain.sharepoint.com/_layouts/15/workbench.aspx)
|
|
@ -0,0 +1,103 @@
|
|||
# Function to connect to the SharePoint Online using cert or credentials
|
||||
function connectSPOnline ($targeturl, $usecert) {
|
||||
if($usecert -eq $true) {
|
||||
# Using cert
|
||||
$tenant = $env:Tenant
|
||||
$clientid = $env:ClientID
|
||||
$thumbprint = $env:Thumbprint
|
||||
# Connect to the root site collections using cert
|
||||
Connect-PnPOnline -Url $targeturl -ClientId $clientid -Thumbprint $thumbprint -Tenant $tenant
|
||||
} else {
|
||||
# Using service account and password
|
||||
$serviceAccount = $env:ServiceAccount
|
||||
$serviceAccountPwd = $env:ServiceAccountPwd
|
||||
# Connect to the root site collections with the service account
|
||||
$encPassword = ConvertTo-SecureString -String $serviceAccountPwd -AsPlainText -Force
|
||||
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $serviceAccount, $encPassword
|
||||
Connect-PnPOnline -Url $targeturl -Credentials $cred
|
||||
}
|
||||
}
|
||||
function uploadToUserProfilePictures ($mysiteurl, $filename, $streamval, $usecert) {
|
||||
connectSPOnline -targeturl $mysiteurl -usecert $usecert
|
||||
Add-PnPFile -Stream $streamval -FileName $filename -Folder $userphotofolder
|
||||
}
|
||||
function updateUserProperty($adminurl, $mysiteurl, $upn, $pictureName, $usecert) {
|
||||
connectSPOnline -targeturl $adminurl -usecert $usecert
|
||||
Set-PnPUserProfileProperty -Account $upn -PropertyName "PictureUrl" -Value "$mysiteurl/$userphotofolder/$pictureName"
|
||||
Set-PnPUserProfileProperty -Account $upn -PropertyName "SPS-PicturePlaceholderState" -Value 0
|
||||
}
|
||||
function getFileAndUpload ($adminurl, $mysiteurl, $targetsiteurl, $userid, $picfolder, $picname, $usecert) {
|
||||
try {
|
||||
connectSPOnline -targeturl $targetsiteurl -usecert $usecert
|
||||
$lfilename = $picname+"_LThumb.jpg"
|
||||
$lpicurl = $picfolder+$lfilename
|
||||
$lfs = (Get-PnPFile -ServerRelativeUrl $lpicurl).OpenBinaryStream()
|
||||
Invoke-PnPQuery
|
||||
uploadToUserProfilePictures -mysiteurl $mysiteurl -filename $lfilename -streamval $lfs.Value
|
||||
|
||||
connectSPOnline -targeturl $targetsiteurl -usecert $usecert
|
||||
$mfilename = $picname+"_MThumb.jpg"
|
||||
$mpicurl = $picfolder+$mfilename
|
||||
$mfs = (Get-PnPFile -ServerRelativeUrl $mpicurl).OpenBinaryStream()
|
||||
Invoke-PnPQuery
|
||||
uploadToUserProfilePictures -mysiteurl $mysiteurl -filename $mfilename -streamval $mfs.Value
|
||||
updateUserProperty -adminurl $adminurl -mysiteurl $mysiteurl -upn $userid -pictureName $mfilename
|
||||
|
||||
connectSPOnline -targeturl $targetsiteurl -usecert $usecert
|
||||
$sfilename = $picname+"_SThumb.jpg"
|
||||
$spicurl = $picfolder+$sfilename
|
||||
$sfs = (Get-PnPFile -ServerRelativeUrl $spicurl).OpenBinaryStream()
|
||||
Invoke-PnPQuery
|
||||
uploadToUserProfilePictures -mysiteurl $mysiteurl -filename $sfilename -streamval $sfs.Value
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
# Read the request as a JSON object
|
||||
$jsoninput = Get-Content $req -Raw | ConvertFrom-Json
|
||||
# Configure local variable
|
||||
$adminurl = $jsoninput.adminurl
|
||||
$mysiteurl = $jsoninput.mysiteurl
|
||||
$targetsiteurl = $jsoninput.targetsiteurl
|
||||
$picfolder = $jsoninput.picfolder
|
||||
$usecert = $jsoninput.usecert
|
||||
$itemid = $jsoninput.itemid
|
||||
$clearPhotos = $jsoninput.clearPhotos
|
||||
$targetList = "UPS Photo Sync Jobs"
|
||||
$userphotofolder = "User Photos/Profile Pictures"
|
||||
# Update the status of the item to 'In-Progress'
|
||||
connectSPOnline -targeturl $targetsiteurl -usecert $usecert
|
||||
Set-PnPListItem -List $targetList -Identity $itemid -Values @{"Status" = "In-Progress" } -SystemUpdate
|
||||
foreach ($val in $jsoninput.value) {
|
||||
if ($null -ne $val.userid -and $null -ne $val.picturename) {
|
||||
$status = getFileAndUpload -adminurl $adminurl -mysiteurl $mysiteurl -targetsiteurl $targetsiteurl -userid $val.userid -picfolder $picfolder -picname $val.picturename -usecert $usecert
|
||||
if ($null -eq $val.Status) {
|
||||
if($true -eq $status) {
|
||||
$val | Add-Member -Name "Status" -Value "Updated" -MemberType NoteProperty
|
||||
} else {
|
||||
$val | Add-Member -Name "Status" -Value "Not Updated" -MemberType NoteProperty
|
||||
}
|
||||
} else {
|
||||
if($true -eq $status) {
|
||||
$val.Status = "Updated"
|
||||
} else {
|
||||
$val.Status = "Not Updated"
|
||||
}
|
||||
}
|
||||
# If the clear photos flag is set delete all the temporary thumbnails
|
||||
connectSPOnline -targeturl $targetsiteurl -usecert $usecert
|
||||
if($true -eq $clearPhotos) {
|
||||
$filename = "{0}{1}{2}" -f $picfolder,$val.picturename,"_LThumb.jpg"
|
||||
Remove-PnPFile -ServerRelativeUrl $filename -Force
|
||||
$filename = "{0}{1}{2}" -f $picfolder,$val.picturename,"_MThumb.jpg"
|
||||
Remove-PnPFile -ServerRelativeUrl $filename -Force
|
||||
$filename = "{0}{1}{2}" -f $picfolder,$val.picturename,"_SThumb.jpg"
|
||||
Remove-PnPFile -ServerRelativeUrl $filename -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
# JSON after updating the properties of the user
|
||||
$jsonOutput = $jsoninput | ConvertTo-Json -depth 100 -Compress
|
||||
Set-PnPListItem -List $targetList -Identity $itemid -Values @{"SyncedData" = $jsonOutput; "Status" = "Completed" } -SystemUpdate
|
Binary file not shown.
After Width: | Height: | Size: 4.6 MiB |
Binary file not shown.
After Width: | Height: | Size: 5.6 MiB |
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"photo-sync-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/photoSync/PhotoSyncWebPart.js",
|
||||
"manifest": "./src/webparts/photoSync/PhotoSyncWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"PhotoSyncWebPartStrings": "lib/webparts/photoSync/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-photo-sync",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "SPUPS Photo Sync",
|
||||
"title": "SPUPS Photo Sync",
|
||||
"id": "1feb523c-4edf-4b85-a8d3-1458ab23fae9",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"isDomainIsolated": false,
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "User.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.Read.All"
|
||||
}
|
||||
],
|
||||
"developer": {
|
||||
"name": "Sudharsan K.",
|
||||
"mpnId": "",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"websiteUrl": "https://spknowledge.com/"
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-photo-sync.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,7 @@
|
|||
'use strict';
|
||||
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
build.initialize(require('gulp'));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "react-photo-sync",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"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/common": "^2.0.6",
|
||||
"@pnp/graph": "^2.0.6",
|
||||
"@pnp/odata": "^2.0.6",
|
||||
"@pnp/sp": "^2.0.6",
|
||||
"@pnp/spfx-controls-react": "1.19.0",
|
||||
"@pnp/spfx-property-controls": "1.19.0",
|
||||
"image-resize": "^1.1.5",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.27.0",
|
||||
"office-ui-fabric-react": "6.214.0",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5",
|
||||
"react-dropzone": "^11.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||
"@microsoft/sp-build-web": "1.11.0",
|
||||
"@microsoft/sp-module-interfaces": "1.11.0",
|
||||
"@microsoft/sp-tslint-rules": "1.11.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.11.0",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1",
|
||||
"jquery": "^3.5.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "08a4b451-7012-4f86-9492-5c3196691cc8",
|
||||
"alias": "PhotoSyncWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart", "SharePointFullPage", "TeamsPersonalApp"],
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "SPUPS Photo Sync" },
|
||||
"description": { "default": "Sync photos from AAD to SharePoint UPS." },
|
||||
"officeFabricIconFontName": "EditPhoto",
|
||||
"properties": {
|
||||
"useFullWidth": false
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import * as strings from 'PhotoSyncWebPartStrings';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
IPropertyPanePage
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
|
||||
import { CalloutTriggers } from '@pnp/spfx-property-controls/lib/PropertyFieldHeader';
|
||||
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
|
||||
import { PropertyFieldToggleWithCallout } from '@pnp/spfx-property-controls/lib/PropertyFieldToggleWithCallout';
|
||||
import { PropertyPaneWebPartInformation } from '@pnp/spfx-property-controls/lib/PropertyPaneWebPartInformation';
|
||||
import { PropertyFieldPeoplePicker, PrincipalType, IPropertyFieldGroupOrPerson } from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
|
||||
|
||||
import { MSGraphClient } from '@microsoft/sp-http';
|
||||
import { sp } from '@pnp/sp';
|
||||
import { ISiteUserInfo } from '@pnp/sp/site-users/types';
|
||||
import { IPhotoSyncProps } from './components/PhotoSync';
|
||||
import PhotoSync from './components/PhotoSync';
|
||||
import Helper, { IHelper } from './common/helper';
|
||||
|
||||
export interface IPhotoSyncWebPartProps {
|
||||
useFullWidth: boolean;
|
||||
appTitle: string;
|
||||
allowedUsers: IPropertyFieldGroupOrPerson[];
|
||||
enableBulkUpdate: boolean;
|
||||
tempLib: string;
|
||||
UseCert: boolean;
|
||||
dateFormat: string;
|
||||
deleteThumbnails: boolean;
|
||||
AzFuncUrl: string;
|
||||
}
|
||||
|
||||
export default class PhotoSyncWebPart extends BaseClientSideWebPart<IPhotoSyncWebPartProps> {
|
||||
private wpPropertyPages: IPropertyPanePage[] = [];
|
||||
private helper: IHelper = null;
|
||||
private client: MSGraphClient = null;
|
||||
|
||||
protected async onInit() {
|
||||
await super.onInit();
|
||||
sp.setup(this.context);
|
||||
this.client = await this.context.msGraphClientFactory.getClient();
|
||||
this.helper = new Helper(this.context.pageContext.web.serverRelativeUrl, '', this.client);
|
||||
}
|
||||
|
||||
public async render(): Promise<void> {
|
||||
const element: React.ReactElement<IPhotoSyncProps> = React.createElement(
|
||||
PhotoSync,
|
||||
{
|
||||
context: this.context,
|
||||
httpClient: this.context.httpClient,
|
||||
siteUrl: this.context.pageContext.legacyPageContext.webAbsoluteUrl,
|
||||
domainName: this.context.pageContext.legacyPageContext.webDomain,
|
||||
displayMode: this.displayMode,
|
||||
helper: this.helper,
|
||||
useFullWidth: this.properties.useFullWidth,
|
||||
appTitle: this.properties.appTitle,
|
||||
updateProperty: (value: string) => {
|
||||
this.properties.appTitle = value;
|
||||
},
|
||||
openPropertyPane: this.openPropertyPane,
|
||||
allowedUsers: this.properties.allowedUsers,
|
||||
enableBulkUpdate: this.properties.enableBulkUpdate,
|
||||
tempLib: this.properties.tempLib,
|
||||
deleteThumbnails: this.properties.deleteThumbnails,
|
||||
UseCert: this.properties.UseCert,
|
||||
dateFormat: this.properties.dateFormat,
|
||||
AzFuncUrl: this.properties.AzFuncUrl
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private openPropertyPane = (): void => {
|
||||
this.context.propertyPane.open();
|
||||
}
|
||||
|
||||
private getUserWPProperties = (): IPropertyPanePage[] => {
|
||||
return [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneWebPartInformation({
|
||||
description: `${strings.PropInfoNormalUser}`,
|
||||
key: 'normalUserInfoId'
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private getAdminWPProperties = (): IPropertyPanePage[] => {
|
||||
return [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyFieldListPicker('tempLib', {
|
||||
key: 'tempLibFieldId',
|
||||
label: strings.PropTempLibLabel,
|
||||
selectedList: this.properties.tempLib,
|
||||
includeHidden: false,
|
||||
orderBy: PropertyFieldListPickerOrderBy.Title,
|
||||
disabled: false,
|
||||
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
|
||||
properties: this.properties,
|
||||
context: this.context,
|
||||
onGetErrorMessage: null,
|
||||
deferredValidationTime: 0,
|
||||
baseTemplate: 101,
|
||||
listsToExclude: ['Documents']
|
||||
}),
|
||||
PropertyPaneWebPartInformation({
|
||||
description: `${strings.PropInfoTempLib}`,
|
||||
key: 'tempLibInfoId'
|
||||
}),
|
||||
PropertyFieldToggleWithCallout('deleteThumbnails', {
|
||||
calloutTrigger: CalloutTriggers.Hover,
|
||||
key: 'deleteThumbnailsFieldId',
|
||||
label: strings.PropDelThumbnail,
|
||||
calloutContent: React.createElement('div', {}, strings.PropDelThumbnailCallout),
|
||||
onText: 'ON',
|
||||
offText: 'OFF',
|
||||
checked: this.properties.deleteThumbnails
|
||||
}),
|
||||
PropertyPaneTextField('AzFuncUrl', {
|
||||
label: strings.PropAzFuncLabel,
|
||||
description: strings.PropAzFuncDesc,
|
||||
multiline: true,
|
||||
placeholder: strings.PropAzFuncLabel,
|
||||
resizable: true,
|
||||
rows: 5,
|
||||
value: this.properties.AzFuncUrl
|
||||
}),
|
||||
PropertyFieldToggleWithCallout('UseCert', {
|
||||
calloutTrigger: CalloutTriggers.Hover,
|
||||
key: 'UseCertFieldId',
|
||||
label: strings.PropUseCertLabel,
|
||||
calloutContent: React.createElement('div', {}, strings.PropUseCertCallout),
|
||||
onText: 'ON',
|
||||
offText: 'OFF',
|
||||
checked: this.properties.UseCert
|
||||
}),
|
||||
PropertyPaneWebPartInformation({
|
||||
description: `${strings.PropInfoUseCert}`,
|
||||
key: 'useCertInfoId'
|
||||
}),
|
||||
PropertyPaneTextField('dateFormat', {
|
||||
label: strings.PropDateFormatLabel,
|
||||
description: '',
|
||||
multiline: false,
|
||||
placeholder: strings.PropDateFormatLabel,
|
||||
resizable: false,
|
||||
value: this.properties.dateFormat
|
||||
}),
|
||||
PropertyPaneWebPartInformation({
|
||||
description: `${strings.PropInfoDateFormat}`,
|
||||
key: 'dateFormatInfoId'
|
||||
}),
|
||||
PropertyFieldPeoplePicker('allowedUsers', {
|
||||
label: 'SharePoint Groups',
|
||||
initialData: this.properties.allowedUsers,
|
||||
allowDuplicate: false,
|
||||
principalType: [PrincipalType.SharePoint],
|
||||
onPropertyChange: this.onPropertyPaneFieldChanged,
|
||||
context: this.context,
|
||||
properties: this.properties,
|
||||
onGetErrorMessage: null,
|
||||
deferredValidationTime: 0,
|
||||
key: 'allowedUsersFieldId'
|
||||
}),
|
||||
PropertyPaneWebPartInformation({
|
||||
description: `${strings.PropAllowedUserInfo}`,
|
||||
key: 'allowedUsersInfoId'
|
||||
}),
|
||||
PropertyFieldToggleWithCallout('enableBulkUpdate', {
|
||||
key: 'enableBulkUpdateFieldId',
|
||||
label: strings.PropEnableBUCallout,
|
||||
onText: 'ON',
|
||||
offText: 'OFF',
|
||||
checked: this.properties.enableBulkUpdate
|
||||
}),
|
||||
PropertyFieldToggleWithCallout('useFullWidth', {
|
||||
key: 'useFullWidthFieldId',
|
||||
label: 'Use page full width',
|
||||
onText: 'ON',
|
||||
offText: 'OFF',
|
||||
checked: this.properties.useFullWidth
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
protected async onPropertyPaneConfigurationStart() {
|
||||
this.context.statusRenderer.displayLoadingIndicator(this.domElement, 'Loading properties...');
|
||||
let currentUserInfo: ISiteUserInfo = await this.helper.getCurrentUserDefaultInfo();
|
||||
if (currentUserInfo.IsSiteAdmin)
|
||||
this.wpPropertyPages = this.getAdminWPProperties();
|
||||
else this.wpPropertyPages = this.getUserWPProperties();
|
||||
this.context.propertyPane.refresh();
|
||||
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: this.wpPropertyPages
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react';
|
||||
import { IHelper } from './helper';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
|
||||
export interface AppContextProps {
|
||||
context: WebPartContext;
|
||||
siteurl: string;
|
||||
domainName: string;
|
||||
helper: IHelper;
|
||||
displayMode: DisplayMode;
|
||||
openPropertyPane: () => void;
|
||||
tempLib: string;
|
||||
deleteThumbnails: boolean;
|
||||
}
|
||||
|
||||
export const AppContext = React.createContext<AppContextProps>(undefined);
|
|
@ -0,0 +1,26 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.MessageContainer{
|
||||
font-style: italic;
|
||||
font-weight: bold !important;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
.errorMessage{
|
||||
color: red !important;
|
||||
padding-top: 10px !important;
|
||||
text-align: center;
|
||||
}
|
||||
.successMessage{
|
||||
color: #64BE1A !important;
|
||||
padding-top: 10px !important;
|
||||
text-align: center;
|
||||
}
|
||||
.warningMessage{
|
||||
color: #BEBB1A !important;
|
||||
padding-top: 10px !important;
|
||||
text-align: center;
|
||||
}
|
||||
.infoMessage{
|
||||
background-color: rgb(148, 210, 230) !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect, useState, useContext } from 'react';
|
||||
import * as strings from 'PhotoSyncWebPartStrings';
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
import { AppContext, AppContextProps } from './AppContext';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import { MessageScope } from './IModel';
|
||||
import { ISiteUserInfo } from '@pnp/sp/site-users/types';
|
||||
|
||||
const ConfigPlaceholder: React.FunctionComponent<{}> = (props) => {
|
||||
const appContext: AppContextProps = useContext(AppContext);
|
||||
const [isSiteAdmin, setSiteAdmin] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const _checkForSiteAdmin = async () => {
|
||||
let currentUserInfo: ISiteUserInfo = await appContext.helper.getCurrentUserDefaultInfo();
|
||||
setSiteAdmin(currentUserInfo.IsSiteAdmin);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
_checkForSiteAdmin();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSiteAdmin ? (
|
||||
<Placeholder iconName='DataManagementSettings'
|
||||
iconText={strings.PlaceholderIconText}
|
||||
description={strings.PlaceholderDescription}
|
||||
buttonLabel={strings.PlaceholderButtonLabel}
|
||||
hideButton={appContext.displayMode === DisplayMode.Read}
|
||||
onConfigure={appContext.openPropertyPane} />
|
||||
) : (
|
||||
<>
|
||||
{loading &&
|
||||
<ProgressIndicator label={strings.SitePrivilegeCheckLabel} description={strings.PropsLoader} />
|
||||
}
|
||||
{!loading &&
|
||||
<MessageContainer MessageScope={MessageScope.SevereWarning} Message={strings.AdminConfigHelp} />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigPlaceholder;
|
|
@ -0,0 +1,85 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../components/PhotoSync.module.scss';
|
||||
import { css } from 'office-ui-fabric-react/lib/Utilities';
|
||||
import { IPersonaSharedProps, Persona, PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
|
||||
export interface IPersonaRenderProps {
|
||||
Title: string;
|
||||
UserID: string;
|
||||
}
|
||||
|
||||
export interface IValueRenderProps {
|
||||
Value: string;
|
||||
}
|
||||
|
||||
export const PersonaRender = (props: IPersonaRenderProps) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: `/_layouts/15/userphoto.aspx?Size=S&Username=${props.UserID}`,
|
||||
text: props.Title,
|
||||
className: styles.divPersona
|
||||
};
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}><Persona {...authorPersona} size={PersonaSize.size24} /></div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SyncTypeRender = (props: IValueRenderProps) => {
|
||||
switch (props.Value.toLowerCase()) {
|
||||
case 'manual':
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}>
|
||||
<div className={css(styles.fieldContent, styles.purplebgColor)}>
|
||||
<span className={css(styles.spnContent, styles.purpleBox)}>{props.Value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'bulk':
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}>
|
||||
<div className={css(styles.fieldContent, styles.yellowbgColor)}>
|
||||
<span className={css(styles.spnContent, styles.yellowBox)}>{props.Value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const StatusRender = (props: IValueRenderProps) => {
|
||||
switch (props.Value.toLowerCase()) {
|
||||
case 'submitted':
|
||||
case 'updated':
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}>
|
||||
<div className={css(styles.fieldContent, styles.bluebgColor)}>
|
||||
<span className={css(styles.spnContent, styles.blueBox)}>{props.Value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'in-progress':
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}>
|
||||
<div className={css(styles.fieldContent, styles.orangebgColor)}>
|
||||
<span className={css(styles.spnContent, styles.orangeBox)}>{props.Value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'completed':
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}>
|
||||
<div className={css(styles.fieldContent, styles.greenbgColor)}>
|
||||
<span className={css(styles.spnContent, styles.greenBox)}>{props.Value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 'error':
|
||||
case 'completed with error':
|
||||
case 'not updated':
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}>
|
||||
<div className={css(styles.fieldContent, styles.redbgColor)}>
|
||||
<span className={css(styles.spnContent, styles.redBox)}>{props.Value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
export enum MessageScope {
|
||||
Success,
|
||||
Failure,
|
||||
Warning,
|
||||
Info,
|
||||
Blocked,
|
||||
SevereWarning
|
||||
}
|
||||
export interface IUserInfo {
|
||||
ID: number;
|
||||
Email: string;
|
||||
LoginName: string;
|
||||
DisplayName: string;
|
||||
Picture: string;
|
||||
IsSiteAdmin: boolean;
|
||||
Groups: string[];
|
||||
}
|
||||
export interface IUserPickerInfo {
|
||||
Title: string;
|
||||
LoginName: string;
|
||||
PhotoUrl: string;
|
||||
AADPhotoUrl?: string;
|
||||
}
|
||||
export interface IAzFuncValues {
|
||||
userid: string;
|
||||
picturename: string;
|
||||
}
|
||||
export enum SyncType {
|
||||
Manual = "Manual",
|
||||
Bulk = "Bulk",
|
||||
}
|
||||
export enum JobStatus {
|
||||
Submitted = "Submitted",
|
||||
InProgress = "In-Progress",
|
||||
Completed = "Completed",
|
||||
CompletedWithError = "Completed With Error",
|
||||
Error = "Error"
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import styles from './CommonStyle.module.scss';
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
import { Text } from 'office-ui-fabric-react/lib/Text';
|
||||
import { MessageScope } from './IModel';
|
||||
|
||||
export interface IMessageContainerProps {
|
||||
Message?: string;
|
||||
MessageScope: MessageScope;
|
||||
ShowDismiss?: boolean;
|
||||
}
|
||||
|
||||
export default function MessageContainer(props: IMessageContainerProps) {
|
||||
const [showMessage, setshowMessage] = React.useState<boolean>(true);
|
||||
const dismissMessage = () => {
|
||||
setshowMessage(false);
|
||||
};
|
||||
const dismiss = props.ShowDismiss ? dismissMessage : null;
|
||||
return (
|
||||
<div className={styles.MessageContainer}>
|
||||
{
|
||||
props.MessageScope === MessageScope.Success && showMessage &&
|
||||
<MessageBar messageBarType={MessageBarType.success} onDismiss={dismiss}>
|
||||
<Text block variant={"mediumPlus"}>{props.Message}</Text>
|
||||
</MessageBar>
|
||||
}
|
||||
{
|
||||
props.MessageScope === MessageScope.Failure && showMessage &&
|
||||
<MessageBar messageBarType={MessageBarType.error} onDismiss={dismiss}>
|
||||
<Text block variant={"mediumPlus"}>{props.Message}</Text>
|
||||
</MessageBar>
|
||||
}
|
||||
{
|
||||
props.MessageScope === MessageScope.Warning && showMessage &&
|
||||
<MessageBar messageBarType={MessageBarType.warning} onDismiss={dismiss}>
|
||||
<Text block variant={"mediumPlus"}>{props.Message}</Text>
|
||||
</MessageBar>
|
||||
}
|
||||
{
|
||||
props.MessageScope === MessageScope.Info && showMessage &&
|
||||
<MessageBar messageBarType={MessageBarType.info} className={styles.infoMessage} onDismiss={dismiss}>
|
||||
<Text block variant={"mediumPlus"}>{props.Message}</Text>
|
||||
</MessageBar>
|
||||
}
|
||||
{
|
||||
props.MessageScope === MessageScope.Blocked && showMessage &&
|
||||
<MessageBar messageBarType={MessageBarType.blocked} onDismiss={dismiss}>
|
||||
<Text block variant={"mediumPlus"}>{props.Message}</Text>
|
||||
</MessageBar>
|
||||
}
|
||||
{
|
||||
props.MessageScope === MessageScope.SevereWarning && showMessage &&
|
||||
<MessageBar messageBarType={MessageBarType.severeWarning} onDismiss={dismiss}>
|
||||
<Text block variant={"mediumPlus"}>{props.Message}</Text>
|
||||
</MessageBar>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,496 @@
|
|||
import { HttpClient, IHttpClientOptions, HttpClientResponse } from '@microsoft/sp-http';
|
||||
import { MSGraphClient } from '@microsoft/sp-http';
|
||||
import "@pnp/graph/users";
|
||||
import "@pnp/graph/photos";
|
||||
import "@pnp/graph/groups";
|
||||
import { sp } from '@pnp/sp';
|
||||
import "@pnp/sp/profiles";
|
||||
import "@pnp/sp/webs";
|
||||
import "@pnp/sp/lists";
|
||||
import "@pnp/sp/items";
|
||||
import "@pnp/sp/fields/list";
|
||||
import "@pnp/sp/views/list";
|
||||
import "@pnp/sp/site-users";
|
||||
import "@pnp/sp/files";
|
||||
import "@pnp/sp/folders";
|
||||
import { Web, IWeb } from "@pnp/sp/webs";
|
||||
import { ISiteUserInfo, ISiteUser } from "@pnp/sp/site-users/types";
|
||||
import { PnPClientStorage, dateAdd } from '@pnp/common';
|
||||
import { IUserInfo, IUserPickerInfo, SyncType, JobStatus, IAzFuncValues } from './IModel';
|
||||
import * as moment from 'moment';
|
||||
import ImageResize from 'image-resize';
|
||||
|
||||
import "@pnp/sp/search";
|
||||
import { SearchQueryBuilder, SearchResults, ISearchQuery } from "@pnp/sp/search";
|
||||
import { ChoiceFieldFormatType } from '@pnp/sp/fields/types';
|
||||
|
||||
const storage = new PnPClientStorage();
|
||||
const imgResize_48 = new ImageResize({ format: 'png', width: 48, height: 48, output: 'base64' });
|
||||
const imgResize_96 = new ImageResize({ format: 'png', width: 96, height: 96, output: 'base64' });
|
||||
const imgResize_240 = new ImageResize({ format: 'png', width: 240, height: 240, output: 'base64' });
|
||||
|
||||
const map: any = require('lodash/map');
|
||||
const intersection: any = require('lodash/intersection');
|
||||
const orderBy: any = require('lodash/orderBy');
|
||||
const chunk: any = require('lodash/chunk');
|
||||
const flattenDeep: any = require('lodash/flattenDeep');
|
||||
|
||||
const batchItemLimit: number = 18;
|
||||
const userBatchLimit: number = 6;
|
||||
|
||||
const userDefStorageKey: string = 'userDefaultInfo';
|
||||
const userCusStorageKey: string = 'userCustomInfo';
|
||||
|
||||
export interface IHelper {
|
||||
getLibraryDetails: (listid: string) => Promise<any>;
|
||||
dataURItoBlob: (dataURI: any) => Blob;
|
||||
getCurrentUserDefaultInfo: () => Promise<ISiteUserInfo>;
|
||||
getCurrentUserCustomInfo: () => Promise<IUserInfo>;
|
||||
checkCurrentUserGroup: (allowedGroups: string[], userGroups: string[]) => boolean;
|
||||
getUsersInfo: (UserIds: string[]) => Promise<any[]>;
|
||||
getUserPhotoFromAADForDisplay: (users: IUserPickerInfo[]) => Promise<any[]>;
|
||||
getAndStoreUserThumbnailPhotos: (users: IUserPickerInfo[], tempLibId: string) => Promise<IAzFuncValues[]>;
|
||||
generateAndStorePhotoThumbnails: (fileInfo: any[], tempLibId: string) => Promise<IAzFuncValues[]>;
|
||||
createSyncItem: (syncType: SyncType) => Promise<number>;
|
||||
updateSyncItem: (itemid: number, inputJson: string) => void;
|
||||
getAllJobs: () => Promise<any[]>;
|
||||
runAzFunction: (httpClient: HttpClient, inputData: any, azFuncUrl: string, itemid: number) => void;
|
||||
checkAndCreateLists: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export default class Helper implements IHelper {
|
||||
private _web: IWeb = null;
|
||||
private _graphClient: MSGraphClient = null;
|
||||
private _graphUrl: string = "https://graph.microsoft.com/v1.0";
|
||||
private web_ServerRelativeURL: string = '';
|
||||
private TPhotoFolderName: string = 'UserPhotos';
|
||||
private Lst_SyncJobs = 'UPS Photo Sync Jobs';
|
||||
|
||||
constructor(webRelativeUrl: string, weburl?: string, graphClient?: MSGraphClient) {
|
||||
this._graphClient = graphClient ? graphClient : null;
|
||||
this._web = weburl ? Web(weburl) : sp.web;
|
||||
this.web_ServerRelativeURL = webRelativeUrl;
|
||||
}
|
||||
/**
|
||||
* Get temp library details
|
||||
* @param listid Temporary library
|
||||
*/
|
||||
public getLibraryDetails = async (listid: string): Promise<string> => {
|
||||
let retFolderPath: string = '';
|
||||
let listDetails = await this._web.lists.getById(listid).get();
|
||||
retFolderPath = listDetails.DocumentTemplateUrl.replace('/Forms/template.dotx', '') + '/' + this.TPhotoFolderName;
|
||||
return retFolderPath;
|
||||
}
|
||||
/**
|
||||
* Check for the template folder, if not creates.
|
||||
*/
|
||||
public checkAndCreateFolder = async (folderPath: string) => {
|
||||
try {
|
||||
await this._web.getFolderByServerRelativeUrl(folderPath).get();
|
||||
} catch (err) {
|
||||
await this._web.folders.add(folderPath);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Convert base64 image to blob.
|
||||
*/
|
||||
public dataURItoBlob = (dataURI): Blob => {
|
||||
// convert base64/URLEncoded data component to raw binary data held in a string
|
||||
var byteString;
|
||||
if (dataURI.split(',')[0].indexOf('base64') >= 0)
|
||||
byteString = atob(dataURI.split(',')[1]);
|
||||
else
|
||||
byteString = unescape(dataURI.split(',')[1]);
|
||||
// separate out the mime component
|
||||
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
||||
// write the bytes of the string to a typed array
|
||||
var ia = new Uint8Array(byteString.length);
|
||||
for (var i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
return new Blob([ia], { type: mimeString });
|
||||
}
|
||||
/**
|
||||
* Get current logged in user default info.
|
||||
*/
|
||||
public getCurrentUserDefaultInfo = async (): Promise<ISiteUserInfo> => {
|
||||
//return await this._web.currentUser.get();
|
||||
let currentUserInfo: ISiteUserInfo = storage.local.get(userDefStorageKey);
|
||||
if (!currentUserInfo) {
|
||||
currentUserInfo = await this._web.currentUser.get();
|
||||
storage.local.put(userDefStorageKey, currentUserInfo, dateAdd(new Date(), 'hour', 1));
|
||||
}
|
||||
return currentUserInfo;
|
||||
}
|
||||
/**
|
||||
* Get current logged in user custom information.
|
||||
*/
|
||||
public getCurrentUserCustomInfo = async (): Promise<IUserInfo> => {
|
||||
let currentUserInfo = await this._web.currentUser.get();
|
||||
let currentUserGroups = await this._web.currentUser.groups.get();
|
||||
return ({
|
||||
ID: currentUserInfo.Id,
|
||||
Email: currentUserInfo.Email,
|
||||
LoginName: currentUserInfo.LoginName,
|
||||
DisplayName: currentUserInfo.Title,
|
||||
IsSiteAdmin: currentUserInfo.IsSiteAdmin,
|
||||
Groups: map(currentUserGroups, 'LoginName'),
|
||||
Picture: '/_layouts/15/userphoto.aspx?size=S&username=' + currentUserInfo.UserPrincipalName,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Check current user is a member of groups or not.
|
||||
*/
|
||||
public checkCurrentUserGroup = (allowedGroups: string[], userGroups: string[]): boolean => {
|
||||
if (userGroups.length > 0) {
|
||||
let diff: string[] = intersection(allowedGroups, userGroups);
|
||||
if (diff && diff.length > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Get user profile photos from Azure AD
|
||||
*/
|
||||
public getUserPhotoFromAADForDisplay = async (users: IUserPickerInfo[]): Promise<any[]> => {
|
||||
return new Promise(async (res, rej) => {
|
||||
if (users && users.length > 0) {
|
||||
let requests: any[] = [];
|
||||
let finalResponse: any[] = [];
|
||||
if (users.length > batchItemLimit) {
|
||||
let chunkUserArr: any[] = chunk(users, batchItemLimit);
|
||||
Promise.all(chunkUserArr.map(async chnkdata => {
|
||||
requests = [];
|
||||
chnkdata.map((user: IUserPickerInfo) => {
|
||||
let upn: string = user.LoginName.split('|')[2];
|
||||
requests.push({
|
||||
id: `${user.LoginName}`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photos/$value`
|
||||
});
|
||||
});
|
||||
let photoReq: any = { requests: requests };
|
||||
let graphRes: any = await this._graphClient.api('$batch').post(photoReq);
|
||||
finalResponse.push(graphRes);
|
||||
})).then(() => {
|
||||
res(finalResponse);
|
||||
});
|
||||
} else {
|
||||
users.map((user: IUserPickerInfo) => {
|
||||
let upn: string = user.LoginName.split('|')[2];
|
||||
requests.push({
|
||||
id: `${user.LoginName}`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photo/$value`
|
||||
});
|
||||
});
|
||||
let photoReq: any = { requests: requests };
|
||||
finalResponse.push(await this._graphClient.api('$batch').post(photoReq));
|
||||
res(finalResponse);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get user info based on UserID
|
||||
*/
|
||||
public getUsersInfo = async (userids: string[]): Promise<any[]> => {
|
||||
return new Promise(async (res, rej) => {
|
||||
let finalResponse: any[] = [];
|
||||
let batch = sp.createBatch();
|
||||
if (userids.length > batchItemLimit) {
|
||||
let chunkUserArr: any[] = chunk(userids, batchItemLimit);
|
||||
Promise.all(chunkUserArr.map(async chnkdata => {
|
||||
batch = sp.createBatch();
|
||||
finalResponse.push(await this.executeBatch(chnkdata, batch));
|
||||
})).then(() => {
|
||||
res(flattenDeep(finalResponse));
|
||||
});
|
||||
} else {
|
||||
batch = sp.createBatch();
|
||||
finalResponse.push(await this.executeBatch(userids, batch));
|
||||
res(flattenDeep(finalResponse));
|
||||
}
|
||||
});
|
||||
}
|
||||
private executeBatch = (chnkdata, batch): Promise<any[]> => {
|
||||
return new Promise((res, rej) => {
|
||||
let finalResponse: any[] = [];
|
||||
batch = sp.createBatch();
|
||||
chnkdata.map((userid: string) => {
|
||||
sp.web.siteUsers.getByLoginName(`i:0#.f|membership|${userid}`).inBatch(batch).get().then((userinfo) => {
|
||||
if (userinfo && userinfo.Title) {
|
||||
finalResponse.push({
|
||||
'loginname': userid,
|
||||
'title': userinfo.Title,
|
||||
'status': 'Valid'
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
finalResponse.push({
|
||||
'loginname': userid,
|
||||
'title': 'User not found!',
|
||||
'status': 'Invalid'
|
||||
});
|
||||
});
|
||||
});
|
||||
batch.execute().then(() => {
|
||||
res(finalResponse);
|
||||
}).catch(() => {
|
||||
res(finalResponse);
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get thumbnail photos for the users.
|
||||
* @param users List of users
|
||||
*/
|
||||
public getAndStoreUserThumbnailPhotos = async (users: IUserPickerInfo[], tempLibId: string): Promise<IAzFuncValues[]> => {
|
||||
let retVals: IAzFuncValues[] = [];
|
||||
return new Promise(async (res, rej) => {
|
||||
let tempLibUrl: string = await this.getLibraryDetails(tempLibId);
|
||||
await this.checkAndCreateFolder(tempLibUrl);
|
||||
if (users && users.length > 0) {
|
||||
let requests: any[] = [];
|
||||
let finalResponse: any[] = [];
|
||||
if (users.length > userBatchLimit) {
|
||||
let chunkUserArr: any[] = chunk(users, userBatchLimit);
|
||||
Promise.all(chunkUserArr.map(async chnkdata => {
|
||||
requests = [];
|
||||
chnkdata.map((user: IUserPickerInfo) => {
|
||||
let upn: string = user.LoginName.split('|')[2];
|
||||
requests.push({
|
||||
id: `${user.LoginName}_1`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photos/48x48/$value`
|
||||
}, {
|
||||
id: `${user.LoginName}_2`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photos/96x96/$value`
|
||||
}, {
|
||||
id: `${user.LoginName}_3`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photos/240x240/$value`
|
||||
});
|
||||
});
|
||||
let photoReq: any = { requests: requests };
|
||||
let graphRes: any = await this._graphClient.api('$batch').post(photoReq);
|
||||
finalResponse.push(graphRes);
|
||||
})).then(async () => {
|
||||
retVals = await this.saveThumbnailPhotosInDocLib(finalResponse, tempLibUrl, "Manual");
|
||||
});
|
||||
} else {
|
||||
users.map((user: IUserPickerInfo) => {
|
||||
let upn: string = user.LoginName.split('|')[2];
|
||||
requests.push({
|
||||
id: `${user.LoginName}_1`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photos/48x48/$value`
|
||||
}, {
|
||||
id: `${user.LoginName}_2`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photos/96x96/$value`
|
||||
}, {
|
||||
id: `${user.LoginName}_3`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: { "Content-Type": "image/jpeg" },
|
||||
url: `/users/${upn}/photos/240x240/$value`
|
||||
});
|
||||
});
|
||||
let photoReq: any = { requests: requests };
|
||||
finalResponse.push(await this._graphClient.api('$batch').post(photoReq));
|
||||
retVals = await this.saveThumbnailPhotosInDocLib(finalResponse, tempLibUrl, "Manual");
|
||||
}
|
||||
}
|
||||
res(retVals);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Add thumbnails to the configured document library
|
||||
*/
|
||||
private saveThumbnailPhotosInDocLib = async (thumbnails: any[], tempLibName: string, scope: 'Manual' | 'Bulk'): Promise<IAzFuncValues[]> => {
|
||||
let retVals: IAzFuncValues[] = [];
|
||||
if (thumbnails && thumbnails.length > 0) {
|
||||
if (scope === "Manual") {
|
||||
thumbnails.map(res => {
|
||||
if (res.responses && res.responses.length > 0) {
|
||||
res.responses.map(async thumbnail => {
|
||||
if (!thumbnail.body.error) {
|
||||
let username: string = thumbnail.id.split('_')[0].split('|')[2];
|
||||
let userFilename: string = username.replace(/[@.]/g, '_');
|
||||
let filecontent = this.dataURItoBlob("data:image/jpg;base64," + thumbnail.body);
|
||||
let partFileName = '';
|
||||
retVals.push({
|
||||
userid: username,
|
||||
picturename: userFilename
|
||||
});
|
||||
if (thumbnail.id.indexOf('_1') > 0) partFileName = 'SThumb.jpg';
|
||||
else if (thumbnail.id.indexOf('_2') > 0) partFileName = "MThumb.jpg";
|
||||
else if (thumbnail.id.indexOf('_3') > 0) partFileName = "LThumb.jpg";
|
||||
await sp.web.getFolderByServerRelativeUrl(decodeURI(`${tempLibName}/`))
|
||||
.files
|
||||
.add(decodeURI(`${tempLibName}/${userFilename}_` + partFileName), filecontent, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return retVals;
|
||||
}
|
||||
if (scope === "Bulk") {
|
||||
return new Promise((res, rej) => {
|
||||
let batch = sp.createBatch();
|
||||
thumbnails.map(async thumbnail => {
|
||||
let username: string = thumbnail.name.replace('.' + thumbnail.name.split('.').pop(), '');
|
||||
let userFilename: string = username.replace(/[@.]/g, '_');
|
||||
retVals.push({
|
||||
userid: username,
|
||||
picturename: userFilename
|
||||
});
|
||||
let filecontent_48 = this.dataURItoBlob(thumbnail.Thumb48);
|
||||
sp.web.getFolderByServerRelativeUrl(decodeURI(`${tempLibName}/`))
|
||||
.files.inBatch(batch)
|
||||
.add(decodeURI(`${tempLibName}/${userFilename}_` + 'SThumb.jpg'), filecontent_48, true);
|
||||
let filecontent_96 = this.dataURItoBlob(thumbnail.Thumb96);
|
||||
sp.web.getFolderByServerRelativeUrl(decodeURI(`${tempLibName}/`))
|
||||
.files.inBatch(batch)
|
||||
.add(decodeURI(`${tempLibName}/${userFilename}_` + 'MThumb.jpg'), filecontent_96, true);
|
||||
let filecontent_240 = this.dataURItoBlob(thumbnail.Thumb240);
|
||||
sp.web.getFolderByServerRelativeUrl(decodeURI(`${tempLibName}/`))
|
||||
.files.inBatch(batch)
|
||||
.add(decodeURI(`${tempLibName}/${userFilename}_` + 'LThumb.jpg'), filecontent_240, true);
|
||||
});
|
||||
batch.execute().then(() => { res(retVals); });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Generate 3 different thumbnails and upload to the temp library.
|
||||
*/
|
||||
public generateAndStorePhotoThumbnails = async (fileInfo: any[], tempLibId: string): Promise<IAzFuncValues[]> => {
|
||||
return new Promise(async (res, rej) => {
|
||||
if (fileInfo && fileInfo.length > 0) {
|
||||
let tempLibUrl: string = await this.getLibraryDetails(tempLibId);
|
||||
Promise.all(fileInfo.map(async file => {
|
||||
file['Thumb48'] = await imgResize_48.play(URL.createObjectURL(file));
|
||||
file['Thumb96'] = await imgResize_96.play(URL.createObjectURL(file));
|
||||
file['Thumb240'] = await imgResize_240.play(URL.createObjectURL(file));
|
||||
})).then(async () => {
|
||||
let users: any = await this.saveThumbnailPhotosInDocLib(fileInfo, tempLibUrl, "Bulk");
|
||||
res(users);
|
||||
}).catch(err => {
|
||||
console.log("Error while generating thumbnails: ", err);
|
||||
res([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create a sync item
|
||||
*/
|
||||
public createSyncItem = async (syncType: SyncType): Promise<number> => {
|
||||
let returnVal: number = 0;
|
||||
let itemAdded = await this._web.lists.getByTitle(this.Lst_SyncJobs).items.add({
|
||||
Title: `SyncJob_${moment().format("MMDDYYYYhhmm")}`,
|
||||
Status: JobStatus.Submitted.toString(),
|
||||
SyncType: syncType.toString()
|
||||
});
|
||||
returnVal = itemAdded.data.Id;
|
||||
return returnVal;
|
||||
}
|
||||
/**
|
||||
* Update Sync item with the input data to sync
|
||||
*/
|
||||
public updateSyncItem = async (itemid: number, inputJson: string) => {
|
||||
await this._web.lists.getByTitle(this.Lst_SyncJobs).items.getById(itemid).update({
|
||||
SyncData: inputJson
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update Sync item with the error status
|
||||
*/
|
||||
public updateSyncItemStatus = async (itemid: number, errMsg: string) => {
|
||||
await this._web.lists.getByTitle(this.Lst_SyncJobs).items.getById(itemid).update({
|
||||
Status: JobStatus.Error,
|
||||
ErrorMessage: errMsg
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get all the jobs items
|
||||
*/
|
||||
public getAllJobs = async (): Promise<any[]> => {
|
||||
return await this._web.lists.getByTitle(this.Lst_SyncJobs).items
|
||||
.select('ID', 'Title', 'SyncedData', 'Status', 'ErrorMessage', 'SyncType', 'Created', 'Author/Title', 'Author/Id', 'Author/EMail')
|
||||
.expand('Author')
|
||||
.getAll();
|
||||
}
|
||||
/**
|
||||
* Azure function to update the UPS Photo properties.
|
||||
*/
|
||||
public runAzFunction = async (httpClient: HttpClient, inputData: any, azFuncUrl: string, itemid: number) => {
|
||||
const requestHeaders: Headers = new Headers();
|
||||
requestHeaders.append("Content-type", "application/json");
|
||||
requestHeaders.append("Cache-Control", "no-cache");
|
||||
const postOptions: IHttpClientOptions = {
|
||||
headers: requestHeaders,
|
||||
body: `${inputData}`
|
||||
};
|
||||
let response: HttpClientResponse = await httpClient.post(azFuncUrl, HttpClient.configurations.v1, postOptions);
|
||||
if (!response.ok) {
|
||||
await this.updateSyncItemStatus(itemid, `${response.status} - ${response.statusText}`);
|
||||
}
|
||||
console.log("Azure Function executed");
|
||||
}
|
||||
/**
|
||||
* Check and create the required lists
|
||||
*/
|
||||
public checkAndCreateLists = async (): Promise<boolean> => {
|
||||
return new Promise<boolean>(async (res, rej) => {
|
||||
try {
|
||||
await this._web.lists.getByTitle(this.Lst_SyncJobs).get();
|
||||
console.log('Sync Jobs List Exists');
|
||||
} catch (err) {
|
||||
console.log("Sync Jobs List doesn't exists, so creating...");
|
||||
await this._createSyncJobsList();
|
||||
console.log("Sync Jobs List created");
|
||||
}
|
||||
console.log("Checked all lists");
|
||||
res(true);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create Sync Jobs list
|
||||
*/
|
||||
public _createSyncJobsList = async () => {
|
||||
let listExists = await (await sp.web.lists.ensure(this.Lst_SyncJobs)).list;
|
||||
await listExists.fields.addMultilineText('SyncData', 6, false, false, false, false, { Required: true, Description: 'Data sent to Azure function for property update.' });
|
||||
await listExists.fields.addMultilineText('SyncedData', 6, false, false, false, false, { Required: true, Description: 'Data received from Azure function with property update status.' });
|
||||
await listExists.fields.addChoice('Status', ['Submitted', 'In-Progress', 'Completed', 'Error'], ChoiceFieldFormatType.Dropdown, false, { Required: true, Description: 'Status of the job.' });
|
||||
await listExists.fields.addChoice('SyncType', ['Manual', 'Bulk'], ChoiceFieldFormatType.Dropdown, false, { Required: true, Description: 'Type of data sent to Azure function.' });
|
||||
await listExists.fields.addMultilineText('ErrorMessage', 6, false, false, false, false, { Required: false, Description: 'Store the error message while calling Azure function.' });
|
||||
let allItemsView = await listExists.views.getByTitle('All Items');
|
||||
let batch = sp.createBatch();
|
||||
allItemsView.fields.inBatch(batch).add('ID');
|
||||
allItemsView.fields.inBatch(batch).add('SyncType');
|
||||
allItemsView.fields.inBatch(batch).add('SyncData');
|
||||
allItemsView.fields.inBatch(batch).add('SyncedData');
|
||||
allItemsView.fields.inBatch(batch).add('Status');
|
||||
allItemsView.fields.inBatch(batch).add('ErrorMessage');
|
||||
allItemsView.fields.inBatch(batch).move('ID', 0);
|
||||
await batch.execute();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect, useState, useContext } from 'react';
|
||||
import { useBoolean } from '@uifabric/react-hooks';
|
||||
import styles from './PhotoSync.module.scss';
|
||||
import * as strings from 'PhotoSyncWebPartStrings';
|
||||
import { AppContext, AppContextProps } from '../common/AppContext';
|
||||
import MessageContainer from '../common/MessageContainer';
|
||||
import { MessageScope, IUserPickerInfo, IAzFuncValues, SyncType } from '../common/IModel';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
import { DetailsList, IColumn, DetailsListLayoutMode, ConstrainMode, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { IPersonaSharedProps, Persona, PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
import { css } from 'office-ui-fabric-react/lib/Utilities';
|
||||
|
||||
const map: any = require('lodash/map');
|
||||
const find: any = require('lodash/find');
|
||||
const filter: any = require('lodash/filter');
|
||||
const uniqBy: any = require('lodash/uniqBy');
|
||||
|
||||
export interface IBulkPhotoSyncProps {
|
||||
updateSPWithPhoto: (data: IAzFuncValues[], itemid: number) => void;
|
||||
}
|
||||
|
||||
const BulkPhotoSync: React.FC<IBulkPhotoSyncProps> = (props) => {
|
||||
const appContext: AppContextProps = useContext(AppContext);
|
||||
const [loading, { setTrue: showLoading, setFalse: hideLoading }] = useBoolean(false);
|
||||
const [columns, setColumns] = useState<IColumn[]>([]);
|
||||
const [showUpdateButton, { setTrue: enableUpdateButton, setFalse: hideUpdateButton }] = useBoolean(false);
|
||||
const [processingPhotoUpdate, { setTrue: showPhotoUpdateProcessing, setFalse: hidePhotoUpdateProcessing }] = useBoolean(false);
|
||||
const [disableUpload, { toggle: toggleDisableUpload }] = useBoolean(false);
|
||||
const [message, setMessage] = useState<any>({ Message: '', Scope: MessageScope.Info });
|
||||
const [clearItems, setclearItems] = useState<boolean>(false);
|
||||
const { getRootProps, getInputProps, fileRejections, acceptedFiles } = useDropzone({
|
||||
accept: 'image/jpeg, image/jpg, image/png',
|
||||
disabled: disableUpload,
|
||||
noClick: disableUpload,
|
||||
noDrag: disableUpload,
|
||||
noDragEventsBubbling: disableUpload,
|
||||
noKeyboard: disableUpload
|
||||
});
|
||||
|
||||
const StatusRender = (childprops) => {
|
||||
switch (childprops.Status.toLowerCase()) {
|
||||
case 'valid':
|
||||
return (
|
||||
<div className={css(styles.fieldContent, styles.greenbgColor)}>
|
||||
<span className={css(styles.spnContent, styles.greenBox)}>{childprops.Status}</span>
|
||||
</div>
|
||||
);
|
||||
case 'invalid':
|
||||
return (
|
||||
<div className={css(styles.fieldContent, styles.redbgColor)}>
|
||||
<span className={css(styles.spnContent, styles.redBox)}>{childprops.Status}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build columns for Datalist.
|
||||
*/
|
||||
const _buildColumns = () => {
|
||||
let cols: IColumn[] = [];
|
||||
let col: string = 'path';
|
||||
cols.push({
|
||||
key: 'loginname', name: 'User ID', fieldName: col, minWidth: 250, maxWidth: 350,
|
||||
onRender: (item: any) => {
|
||||
return (<div className={styles.fieldCustomizer}>{item[col].replace('.' + item[col].split('.').pop(), '')}</div>);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'usertitle', name: 'Title', fieldName: 'title', minWidth: 250, maxWidth: 350,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: `/_layouts/15/userphoto.aspx?Size=S&username=${item.name.replace('.' + item.name.split('.').pop(), '')}`,
|
||||
text: item.title,
|
||||
className: styles.divPersona
|
||||
};
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}><Persona {...authorPersona} size={PersonaSize.size24} /></div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'preview', name: 'Photo', fieldName: col, minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (
|
||||
<div className={styles.fieldCustomizer}>
|
||||
<img style={{ width: '50px' }} src={URL.createObjectURL(item)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'status', name: 'Status', fieldName: 'status', minWidth: 250, maxWidth: 350,
|
||||
onRender: (item: any) => {
|
||||
return (<div className={styles.fieldCustomizer}><StatusRender Status={item.status} /></div>);
|
||||
}
|
||||
} as IColumn);
|
||||
setColumns(cols);
|
||||
};
|
||||
const _listUploadedFiles = async () => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
showLoading();
|
||||
let userids: string[] = map(acceptedFiles, (o) => { return o.name.replace('.' + o.name.split('.').pop(), ''); });
|
||||
let userinfo: any[] = await appContext.helper.getUsersInfo(userids);
|
||||
console.log(userinfo);
|
||||
if (userinfo && userinfo.length > 0) {
|
||||
userinfo.map((user: any) => {
|
||||
let fil: any = find(acceptedFiles, (o) => { return o.name.replace('.' + o.name.split('.').pop(), '') == user.loginname; });
|
||||
if (fil) {
|
||||
fil['title'] = user.title;
|
||||
fil['status'] = user.status;
|
||||
}
|
||||
});
|
||||
}
|
||||
_buildColumns();
|
||||
hideLoading();
|
||||
hidePhotoUpdateProcessing();
|
||||
enableUpdateButton();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* To generate the photo thumbnails and upload to the temp library.
|
||||
* To send the updated final json to the Azure function to trigger the job for photo sync
|
||||
*/
|
||||
const _syncPhotoToSPUPS = async () => {
|
||||
showPhotoUpdateProcessing();
|
||||
toggleDisableUpload();
|
||||
let finalFiles = filter(acceptedFiles, (o) => { return o.status.toLowerCase() == "valid"; });
|
||||
console.log(finalFiles);
|
||||
let userVals: IAzFuncValues[] = await appContext.helper.generateAndStorePhotoThumbnails(finalFiles, appContext.tempLib);
|
||||
let itemID = await appContext.helper.createSyncItem(SyncType.Bulk);
|
||||
await props.updateSPWithPhoto(uniqBy(userVals, 'userid'), itemID);
|
||||
toggleDisableUpload();
|
||||
hidePhotoUpdateProcessing();
|
||||
hideUpdateButton();
|
||||
setclearItems(true);
|
||||
setMessage({ Message: strings.UpdateProcessInitialized, Scope: MessageScope.Success });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMessage({ Message: '' });
|
||||
setclearItems(false);
|
||||
_listUploadedFiles();
|
||||
}, [acceptedFiles]);
|
||||
return (
|
||||
<div>
|
||||
<div style={{ margin: '5px 0px' }}>
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.BulkSyncNote} />
|
||||
</div>
|
||||
<section className={styles.dropZoneContainer}>
|
||||
<div {...getRootProps({ className: css(styles.dropzone, disableUpload ? styles.dropZonedisabled : '') })}>
|
||||
<input {...getInputProps()} />
|
||||
<p>{strings.BulkPhotoDragDrop}</p>
|
||||
</div>
|
||||
</section>
|
||||
{loading &&
|
||||
<ProgressIndicator label="Loading Photos..." description="Please wait..." />
|
||||
}
|
||||
{!loading && message && message.Message && message.Message.length > 0 &&
|
||||
<MessageContainer MessageScope={message.Scope} Message={message.Message} />
|
||||
}
|
||||
{!loading && !clearItems && acceptedFiles && acceptedFiles.length > 0 &&
|
||||
<>
|
||||
<div className={styles.detailsListContainer}>
|
||||
<DetailsList
|
||||
items={clearItems ? [] : acceptedFiles}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true} />
|
||||
</div>
|
||||
{showUpdateButton &&
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<PrimaryButton text={strings.BtnUpdatePhoto} onClick={_syncPhotoToSPUPS} disabled={processingPhotoUpdate} />
|
||||
{processingPhotoUpdate && <Spinner className={styles.generateTemplateLoader} label={strings.PropsLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkPhotoSync;
|
|
@ -0,0 +1,168 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.photoSync {
|
||||
.container {
|
||||
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);
|
||||
}
|
||||
|
||||
.row {
|
||||
@include ms-Grid-row;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.column {
|
||||
@include ms-Grid-col;
|
||||
@include ms-lg10;
|
||||
@include ms-xl10;
|
||||
@include ms-xlPush2;
|
||||
@include ms-lgPush1;
|
||||
}
|
||||
.periodmenu {
|
||||
margin-bottom: 15px !important;
|
||||
width: 85%;
|
||||
display: inline-block;
|
||||
}
|
||||
.generateTemplateLoader {
|
||||
display: inline-flex;
|
||||
margin-top: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.note {
|
||||
font-size: 13px;
|
||||
font-family: initial;
|
||||
margin-top: 5px;
|
||||
font-style: italic;
|
||||
}
|
||||
.noPhotoMsg {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
position: relative;
|
||||
top: 30%;
|
||||
}
|
||||
.detailsListContainer {
|
||||
margin-top: 5px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dropZonedisabled {
|
||||
border-color: #CCC !important;
|
||||
background-color: lightgrey !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.dropZoneContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: sans-serif;
|
||||
.dropzone {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-width: 2px;
|
||||
border-radius: 3px;
|
||||
border-color: "[theme:themeLighter]";
|
||||
border-style: dashed;
|
||||
background-color: "[theme:themeLighter]";
|
||||
color: "[theme:themeWhite]";
|
||||
outline: none;
|
||||
font-weight: bold;
|
||||
transition: border .24s ease-in-out;
|
||||
&:hover, &::after {
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
border-color: "[theme:themePrimary]";
|
||||
transition: border .24s ease-in-out;
|
||||
}
|
||||
}
|
||||
.thumbsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
.thumb {
|
||||
display: inline-flex;
|
||||
border-radius: 2;
|
||||
border: 1px solid #eaeaea;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
.thumbInner {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fieldCustomizer {
|
||||
min-height: 30px;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
.divPersona {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
padding-right: 8px;
|
||||
margin: 2px;
|
||||
background-color: #edebe9;
|
||||
}
|
||||
.divPersona div:nth-child(2) {
|
||||
padding: 0px 5px !important;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.fieldContent {
|
||||
box-sizing: border-box;
|
||||
padding: 4px 8px 5px 8px;
|
||||
display: flex;
|
||||
border-radius: 16px;
|
||||
min-height: 24px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
margin-right: 4px;
|
||||
.spnContent {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.fieldContent.redbgColor {background-color: #fbd3d3;}
|
||||
.fieldContent.greenbgColor {background-color: #baf3ba;}
|
||||
.fieldContent.orangebgColor {background-color: #ffc1ae;}
|
||||
.fieldContent.bluebgColor {background-color: #c8faff;}
|
||||
.fieldContent.purplebgColor {background-color: #c0b1e6;}
|
||||
.fieldContent.yellowbgColor {background-color: #fbe3c4;}
|
||||
}
|
||||
.redBox {color: rgb(161, 10, 10);}
|
||||
.greenBox{color: rgb(7, 133, 7);}
|
||||
.orangeBox {color: #e35e32;}
|
||||
.blueBox {color: #0c90b1;}
|
||||
.purpleBox {color: #2d0988;}
|
||||
.yellowBox {color: rgb(126, 75, 9);}
|
||||
.searchcontainer {
|
||||
width: 95%;
|
||||
display: inline-block;
|
||||
}
|
||||
.refreshContainer {
|
||||
display: inline-flex;
|
||||
margin-top: 6px;
|
||||
margin-left: 5px;
|
||||
position: absolute;
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from './PhotoSync.module.scss';
|
||||
import * as strings from 'PhotoSyncWebPartStrings';
|
||||
import { HttpClient } from '@microsoft/sp-http';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot';
|
||||
import { AppContext, AppContextProps } from '../common/AppContext';
|
||||
import { IHelper } from '../common/helper';
|
||||
import ConfigPlaceholder from '../common/ConfigPlaceholder';
|
||||
import { IPropertyFieldGroupOrPerson } from '@pnp/spfx-property-controls/lib/propertyFields/peoplePicker';
|
||||
import MessageContainer from '../common/MessageContainer';
|
||||
import { MessageScope, IUserInfo, IAzFuncValues } from '../common/IModel';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import UserSelectionSync from './UserSelectionSync';
|
||||
import BulkPhotoSync from './BulkPhotoSync';
|
||||
import SyncJobs from './SyncJobs';
|
||||
|
||||
|
||||
const map: any = require('lodash/map');
|
||||
|
||||
export interface IPhotoSyncProps {
|
||||
context: WebPartContext;
|
||||
httpClient: HttpClient;
|
||||
siteUrl: string;
|
||||
domainName: string;
|
||||
helper: IHelper;
|
||||
displayMode: DisplayMode;
|
||||
useFullWidth: boolean;
|
||||
appTitle: string;
|
||||
updateProperty: (value: string) => void;
|
||||
AzFuncUrl: string;
|
||||
UseCert: boolean;
|
||||
dateFormat: string;
|
||||
allowedUsers: IPropertyFieldGroupOrPerson[];
|
||||
openPropertyPane: () => void;
|
||||
enableBulkUpdate: boolean;
|
||||
tempLib: string;
|
||||
deleteThumbnails: boolean;
|
||||
}
|
||||
|
||||
const PhotoSync: React.FunctionComponent<IPhotoSyncProps> = (props) => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [accessDenied, setAccessDenied] = useState<boolean>(false);
|
||||
const [listExists, setListExists] = useState<boolean>(false);
|
||||
const [selectedMenu, setSelectedMenu] = useState<string>('0');
|
||||
const [pivotItems, setPivotItems] = useState<any[]>([]);
|
||||
const [disablePivot, setdisablePivot] = useState<boolean>(false);
|
||||
const headerButtonProps = { 'disabled': disablePivot };
|
||||
|
||||
const parentCtxValues: AppContextProps = {
|
||||
context: props.context,
|
||||
siteurl: props.siteUrl,
|
||||
domainName: props.domainName,
|
||||
helper: props.helper,
|
||||
displayMode: props.displayMode,
|
||||
openPropertyPane: props.openPropertyPane,
|
||||
tempLib: props.tempLib,
|
||||
deleteThumbnails: props.deleteThumbnails
|
||||
};
|
||||
const showConfig = !props.tempLib; //!props.templateLib || !props.AzFuncUrl || !props.tempLib ? true : false;
|
||||
const _useFullWidth = () => {
|
||||
const jQuery: any = require('jquery');
|
||||
if (props.useFullWidth) {
|
||||
jQuery("#workbenchPageContent").prop("style", "max-width: none");
|
||||
jQuery(".SPCanvas-canvas").prop("style", "max-width: none");
|
||||
jQuery(".CanvasZone").prop("style", "max-width: none");
|
||||
} else {
|
||||
jQuery("#workbenchPageContent").prop("style", "max-width: 924px");
|
||||
}
|
||||
};
|
||||
const _checkAndCreateLists = async () => {
|
||||
setLoading(false);
|
||||
let listCheck: boolean = await props.helper.checkAndCreateLists();
|
||||
if (listCheck) setListExists(true);
|
||||
};
|
||||
const _checkForAccess = async () => {
|
||||
setLoading(true);
|
||||
let currentUserInfo: IUserInfo = await props.helper.getCurrentUserCustomInfo();
|
||||
if (currentUserInfo.IsSiteAdmin) {
|
||||
_checkAndCreateLists();
|
||||
} else {
|
||||
let allowedGroups: string[] = map(props.allowedUsers, 'login');
|
||||
let accessAllowed: boolean = props.helper.checkCurrentUserGroup(allowedGroups, currentUserInfo.Groups);
|
||||
console.log("Access allowed: ", accessAllowed);
|
||||
if (accessAllowed) {
|
||||
_checkAndCreateLists();
|
||||
} else {
|
||||
setLoading(false);
|
||||
setAccessDenied(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
const _updatePivotMenus = () => {
|
||||
let pvitems: any[] = [];
|
||||
if (props.enableBulkUpdate) {
|
||||
pvitems = [
|
||||
<PivotItem headerText={strings.TabMenu2} itemKey="1" itemIcon="BulkUpload" headerButtonProps={headerButtonProps}></PivotItem>,
|
||||
];
|
||||
}
|
||||
setPivotItems(pvitems);
|
||||
};
|
||||
const _onMenuClick = (item?: PivotItem, ev?: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||
if (item) {
|
||||
if (item.props.itemKey == "0") {
|
||||
|
||||
} else if (item.props.itemKey == "1") {
|
||||
|
||||
}
|
||||
setSelectedMenu(item.props.itemKey);
|
||||
}
|
||||
};
|
||||
const _prepareJSONForAzFunc = (data: IAzFuncValues[], itemid: number, folderPath: string): string => {
|
||||
let finalJson: string = "";
|
||||
let tenantName: string = props.siteUrl.split("." + props.domainName)[0];
|
||||
if (data && data.length > 0) {
|
||||
let userPhotoObj = new Object();
|
||||
userPhotoObj['adminurl'] = `${tenantName}-admin.${props.domainName}`;
|
||||
userPhotoObj['mysiteurl'] = `${tenantName}-my.${props.domainName}`;
|
||||
userPhotoObj['targetSiteUrl'] = props.siteUrl;
|
||||
userPhotoObj['picfolder'] = folderPath + "/";
|
||||
userPhotoObj['clearPhotos'] = props.deleteThumbnails;
|
||||
userPhotoObj['usecert'] = props.UseCert ? props.UseCert : false;
|
||||
userPhotoObj['itemId'] = itemid;
|
||||
userPhotoObj['value'] = data;
|
||||
finalJson = JSON.stringify(userPhotoObj);
|
||||
}
|
||||
return finalJson;
|
||||
};
|
||||
const _updateSPWithPhoto = async (data: IAzFuncValues[], itemid: number) => {
|
||||
setdisablePivot(true);
|
||||
let tempFolderPath: string = await props.helper.getLibraryDetails(props.tempLib);
|
||||
let finalJson: string = _prepareJSONForAzFunc(data, itemid, tempFolderPath);
|
||||
await props.helper.updateSyncItem(itemid, finalJson);
|
||||
props.helper.runAzFunction(props.httpClient, finalJson, props.AzFuncUrl, itemid);
|
||||
setdisablePivot(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
_useFullWidth();
|
||||
}, [props.useFullWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
_checkForAccess();
|
||||
}, [props.allowedUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
_updatePivotMenus();
|
||||
}, [props.enableBulkUpdate]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={parentCtxValues}>
|
||||
<div className={styles.photoSync}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.column}>
|
||||
<WebPartTitle displayMode={props.displayMode} title={props.appTitle ? props.appTitle : strings.DefaultAppTitle} updateProperty={props.updateProperty} />
|
||||
{showConfig ? (
|
||||
<ConfigPlaceholder />
|
||||
) : (
|
||||
<>
|
||||
{loading ? (
|
||||
<ProgressIndicator label={strings.AccessCheckDesc} description={strings.PropsLoader} />
|
||||
) : (
|
||||
<>
|
||||
{accessDenied ? (
|
||||
<MessageContainer MessageScope={MessageScope.SevereWarning} Message={strings.AccessDenied} />
|
||||
) : (
|
||||
<>
|
||||
{!listExists ? (
|
||||
<ProgressIndicator label={strings.ListCreationText} description={strings.PropsLoader} />
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Pivot defaultSelectedKey="0" selectedKey={selectedMenu} onLinkClick={_onMenuClick} className={styles.periodmenu}>
|
||||
<PivotItem headerText={strings.TabMenu1} itemKey="0" itemIcon="SchoolDataSyncLogo" headerButtonProps={headerButtonProps}></PivotItem>
|
||||
{pivotItems}
|
||||
<PivotItem headerText={strings.TabMenu3} itemKey="2" itemIcon="SyncStatus" headerButtonProps={headerButtonProps}></PivotItem>
|
||||
</Pivot>
|
||||
</div>
|
||||
{/* Individual Selection photo sync */}
|
||||
{selectedMenu == "0" &&
|
||||
<div>
|
||||
<UserSelectionSync updateSPWithPhoto={_updateSPWithPhoto} />
|
||||
</div>
|
||||
}
|
||||
{/* Bulk photo sync */}
|
||||
{selectedMenu == "1" &&
|
||||
<BulkPhotoSync updateSPWithPhoto={_updateSPWithPhoto} />
|
||||
}
|
||||
{/* Overall status of the sync jobs */}
|
||||
{selectedMenu == "2" &&
|
||||
<SyncJobs dateFormat={props.dateFormat} />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhotoSync;
|
|
@ -0,0 +1,93 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as strings from 'PhotoSyncWebPartStrings';
|
||||
import { DetailsList, IColumn, DetailsListLayoutMode, ConstrainMode, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
import MessageContainer from '../common/MessageContainer';
|
||||
import { MessageScope } from '../common/IModel';
|
||||
import { StatusRender, PersonaRender } from '../common/FieldRenderer';
|
||||
|
||||
const map: any = require('lodash/map');
|
||||
const union: any = require('lodash/union');
|
||||
|
||||
export interface ISyncJobResultsProps {
|
||||
data: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const SyncJobResults: React.FC<ISyncJobResultsProps> = (props) => {
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [jobresults, setJobResults] = React.useState<any[]>([]);
|
||||
const [columns, setColumns] = React.useState<IColumn[]>([]);
|
||||
const [emptyMessage, setEmptyMessage] = useState<boolean>(false);
|
||||
|
||||
const _buildColumns = (colValues: string[]) => {
|
||||
let cols: IColumn[] = [];
|
||||
colValues.map(col => {
|
||||
if (col.toLowerCase() == "userid") {
|
||||
cols.push({
|
||||
key: 'UserID', name: 'User ID', fieldName: col, minWidth: 300,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return <PersonaRender Title={item[col]} UserID={item[col]} />;
|
||||
|
||||
}
|
||||
} as IColumn);
|
||||
} else {
|
||||
cols.push({
|
||||
key: col, name: col, fieldName: col, minWidth: 100, maxWidth: 250,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return <StatusRender Value={item[col]} />;
|
||||
}
|
||||
} as IColumn);
|
||||
}
|
||||
});
|
||||
setColumns(cols);
|
||||
};
|
||||
|
||||
const _buildJobResults = () => {
|
||||
if (props.error && props.error.length > 0) {
|
||||
|
||||
} else {
|
||||
if (props.data && props.data.length > 0) {
|
||||
let parsedResults = JSON.parse(props.data);
|
||||
let colValues = ['userid', 'Status'];
|
||||
_buildColumns(colValues);
|
||||
setJobResults(parsedResults.value);
|
||||
} else setEmptyMessage(true);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
_buildJobResults();
|
||||
}, [props.data]);
|
||||
|
||||
return (
|
||||
<div style={{ maxHeight: '600', maxWidth: '600', overflow: 'auto' }}>
|
||||
{loading &&
|
||||
<ProgressIndicator label={strings.PropsLoader} description={strings.JobResultsLoaderDesc} />
|
||||
}
|
||||
{!loading && jobresults && jobresults.length > 0 &&
|
||||
<DetailsList
|
||||
items={jobresults}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true}
|
||||
/>
|
||||
}
|
||||
{props.error && props.error.length > 0 &&
|
||||
<MessageContainer MessageScope={MessageScope.Failure} Message={`${strings.SyncFailedErrorMessage} ${props.error}`} />
|
||||
}
|
||||
{emptyMessage &&
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={`${strings.EmptyTable}`} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncJobResults;
|
|
@ -0,0 +1,200 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect, useState, useContext } from 'react';
|
||||
import { useBoolean } from '@uifabric/react-hooks';
|
||||
import styles from './PhotoSync.module.scss';
|
||||
import * as strings from 'PhotoSyncWebPartStrings';
|
||||
import { DetailsList, IColumn, DetailsListLayoutMode, ConstrainMode, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
|
||||
import { ActionButton, IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { IIconProps } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { Dialog } from 'office-ui-fabric-react/lib/components/Dialog/Dialog';
|
||||
import { DialogType } from 'office-ui-fabric-react/lib/components/Dialog';
|
||||
import * as moment from 'moment';
|
||||
import { AppContext, AppContextProps } from '../common/AppContext';
|
||||
import MessageContainer from '../common/MessageContainer';
|
||||
import SyncJobResults from './SyncJobResults';
|
||||
import { MessageScope } from '../common/IModel';
|
||||
import { PersonaRender, StatusRender, SyncTypeRender } from '../common/FieldRenderer';
|
||||
|
||||
const orderBy: any = require('lodash/orderBy');
|
||||
const filter: any = require('lodash/filter');
|
||||
|
||||
export interface ISyncJobsProps {
|
||||
dateFormat: string;
|
||||
}
|
||||
|
||||
const SyncJobs: React.FC<ISyncJobsProps> = (props) => {
|
||||
const appContext: AppContextProps = useContext(AppContext);
|
||||
const actionIcon: IIconProps = { iconName: 'InfoSolid' };
|
||||
const refreshIcon: IIconProps = { iconName: 'Refresh' };
|
||||
|
||||
const [refreshLoading, setRefreshLoading] = useState<boolean>(false);
|
||||
const [loading, { setTrue: showLoading, setFalse: hideLoading }] = useBoolean(true);
|
||||
const [jobs, setJobs] = useState<any[]>([]);
|
||||
const [columns, setColumns] = useState<IColumn[]>([]);
|
||||
const [filItems, setFilItems] = useState<any[]>([]);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [hideDialog, setHideDialog] = React.useState<boolean>(true);
|
||||
const [jobresults, setJobResults] = React.useState<string>('');
|
||||
const [errorMsg, setErrorMessage] = React.useState<string>('');
|
||||
|
||||
const actionClick = (data) => {
|
||||
setJobResults(data.SyncResults);
|
||||
setErrorMessage(data.ErrorMessage);
|
||||
setHideDialog(false);
|
||||
};
|
||||
|
||||
const ActionRender = (actionProps) => {
|
||||
return (
|
||||
<ActionButton iconProps={actionIcon} allowDisabledFocus onClick={() => { actionClick(actionProps); }} disabled={actionProps.disabled} />
|
||||
);
|
||||
};
|
||||
|
||||
const _buildColumns = () => {
|
||||
let cols: IColumn[] = [];
|
||||
cols.push({
|
||||
key: 'ID', name: 'ID', fieldName: 'ID', minWidth: 50, maxWidth: 50,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (<div className={styles.fieldCustomizer}>{item.ID}</div>);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Title', name: 'Title', fieldName: 'Title', minWidth: 100, maxWidth: 150,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (<div className={styles.fieldCustomizer}>{item.Title}</div>);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'SyncType', name: 'Sync Type', fieldName: 'SyncType', minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return <SyncTypeRender Value={item.SyncType} />;
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Author', name: 'Author', fieldName: 'Author.Title', minWidth: 250, maxWidth: 250,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return <PersonaRender Title={item["Author"].Title} UserID={item["Author"].EMail} />;
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Created', name: 'Created', fieldName: 'Created', minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (<div className={styles.fieldCustomizer}>{moment(item.Created).format(props.dateFormat ? props.dateFormat : 'DD/MM/YYYY')}</div>);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Status', name: 'Status', fieldName: 'Status', minWidth: 100, maxWidth: 150,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return <StatusRender Value={item.Status} />;
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Actions', name: 'Actions', fieldName: 'ID', minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
let disabled: boolean = ((item.Status.toLowerCase() == "error" && item.ErrorMessage && item.ErrorMessage.length > 0) || item.Status.toLowerCase().indexOf('completed') >= 0) ? false : true;
|
||||
return (<ActionRender SyncResults={item.SyncedData} ErrorMessage={item.ErrorMessage} disabled={disabled} />);
|
||||
}
|
||||
});
|
||||
setColumns(cols);
|
||||
};
|
||||
|
||||
const _onChangeSearchBox = (srchkey: string) => {
|
||||
setSearchText(srchkey);
|
||||
if (srchkey && srchkey.length > 0) {
|
||||
let filtered: any[] = filter(jobs, (o) => {
|
||||
return o.ID.toString().indexOf(srchkey.toLowerCase()) > -1 ||
|
||||
o.Title.toLowerCase().indexOf(srchkey.toLowerCase()) > -1 || o['Author'].Title.toLowerCase().indexOf(srchkey.toLowerCase()) > -1 ||
|
||||
o.Status.toLowerCase().indexOf(srchkey.toLowerCase()) > -1 || o.SyncType.toLowerCase().indexOf(srchkey.toLowerCase()) > -1;
|
||||
});
|
||||
setFilItems(filtered);
|
||||
} else setFilItems(jobs);
|
||||
};
|
||||
|
||||
const _loadJobsList = async () => {
|
||||
let jobsList: any[] = await appContext.helper.getAllJobs();
|
||||
jobsList = orderBy(jobsList, ['ID'], ['desc']);
|
||||
setJobs(jobsList);
|
||||
setFilItems(jobsList);
|
||||
};
|
||||
|
||||
const _buildJobsList = async () => {
|
||||
_buildColumns();
|
||||
await _loadJobsList();
|
||||
hideLoading();
|
||||
};
|
||||
|
||||
const _refreshList = async () => {
|
||||
setRefreshLoading(true);
|
||||
await _loadJobsList();
|
||||
setRefreshLoading(false);
|
||||
};
|
||||
|
||||
const _closeDialog = () => {
|
||||
setHideDialog(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
_buildJobsList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading ? (
|
||||
<ProgressIndicator label={strings.PropsLoader} description={strings.JobsListLoaderDesc} />
|
||||
) : (
|
||||
<div className="ms-Grid-row" style={{ marginBottom: '5px', paddingLeft: '18px' }}>
|
||||
<div className={styles.searchcontainer}>
|
||||
<SearchBox
|
||||
placeholder={`Search...`}
|
||||
onChange={_onChangeSearchBox}
|
||||
underlined={true}
|
||||
iconProps={{ iconName: 'Filter' }}
|
||||
value={searchText}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.refreshContainer}>
|
||||
<IconButton iconProps={refreshIcon} title="Refresh" ariaLabel="Refresh" onClick={_refreshList} disabled={refreshLoading} />
|
||||
{refreshLoading &&
|
||||
<Spinner size={SpinnerSize.small} />
|
||||
}
|
||||
</div>
|
||||
{filItems && filItems.length > 0 ? (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<DetailsList
|
||||
items={filItems}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.EmptyTable} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Dialog hidden={hideDialog} onDismiss={_closeDialog} minWidth='400' maxWidth='700'
|
||||
dialogContentProps={{
|
||||
type: DialogType.close,
|
||||
title: `${strings.JobResultsDialogTitle}`
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
isDarkOverlay: true,
|
||||
styles: { main: { minWidth: 400, maxHeight: 700 } },
|
||||
}}>
|
||||
<SyncJobResults data={jobresults} error={errorMsg} />
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncJobs;
|
|
@ -0,0 +1,203 @@
|
|||
import * as React from 'react';
|
||||
import { useEffect, useState, useContext } from 'react';
|
||||
import { useBoolean } from '@uifabric/react-hooks';
|
||||
import styles from './PhotoSync.module.scss';
|
||||
import * as strings from 'PhotoSyncWebPartStrings';
|
||||
import { PeoplePicker, PrincipalType } from "@pnp/spfx-controls-react/lib/PeoplePicker";
|
||||
import { MessageScope, IUserPickerInfo, IAzFuncValues, SyncType } from '../common/IModel';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { DetailsList, IColumn, DetailsListLayoutMode, ConstrainMode, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { IPersonaSharedProps, Persona, PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
import { AppContext, AppContextProps } from '../common/AppContext';
|
||||
import MessageContainer from '../common/MessageContainer';
|
||||
|
||||
const filter: any = require('lodash/filter');
|
||||
const map: any = require('lodash/map');
|
||||
const uniqBy: any = require('lodash/uniqBy');
|
||||
|
||||
export interface IUserSelectionSyncProps {
|
||||
updateSPWithPhoto: (data: IAzFuncValues[], itemid: number) => void;
|
||||
}
|
||||
|
||||
const UserSelectionSync: React.FunctionComponent<IUserSelectionSyncProps> = (props) => {
|
||||
const appContext: AppContextProps = useContext(AppContext);
|
||||
const [selectedUsers, setSelectedUsers] = useState<any[]>([]);
|
||||
const [showPhotoLoader, { toggle: togglePhotoLoader, setFalse: hidePhotoLoader }] = useBoolean(false);
|
||||
const [disableButton, { toggle: toggleDisableButton, setFalse: enableButton }] = useBoolean(false);
|
||||
const [disableUserPicker, { toggle: toggleDisableUserPicker }] = useBoolean(false);
|
||||
const [columns, setColumns] = useState<IColumn[]>([]);
|
||||
const [processingPhotoUpdate, { toggle: toggleProcessingPhotoUpdate }] = useBoolean(false);
|
||||
const [showUpdateButton, { toggle: toggleShowUpdateButton, setFalse: hideUpdateButton }] = useBoolean(false);
|
||||
const [message, setMessage] = useState<any>({ Message: '', Scope: MessageScope.Info });
|
||||
|
||||
/**
|
||||
* Build columns for Datalist.
|
||||
*/
|
||||
const _buildColumns = (colValues: string[]) => {
|
||||
let cols: IColumn[] = [];
|
||||
colValues.map(col => {
|
||||
if (col.toLowerCase() == "title") {
|
||||
cols.push({
|
||||
key: 'title', name: 'Title', fieldName: col, minWidth: 150, maxWidth: 200,
|
||||
} as IColumn);
|
||||
}
|
||||
if (col.toLowerCase() == "loginname") {
|
||||
cols.push({
|
||||
key: 'loginname', name: 'User ID', fieldName: col, minWidth: 250, maxWidth: 350,
|
||||
onRender: (item: any) => {
|
||||
return (<span>{item[col].split('|')[2]}</span>);
|
||||
}
|
||||
} as IColumn);
|
||||
}
|
||||
if (col.toLowerCase() == "photourl") {
|
||||
cols.push({
|
||||
key: 'photourl', name: 'SP Profile Photo', fieldName: col, minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: item[col],
|
||||
};
|
||||
return (
|
||||
<div><Persona {...authorPersona} size={PersonaSize.large} /></div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
}
|
||||
if (col.toLowerCase() == "aadphotourl") {
|
||||
cols.push({
|
||||
key: 'aadphotourl', name: 'Azure Profile Photo', fieldName: col, minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
if (item[col]) {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: item[col],
|
||||
};
|
||||
return (
|
||||
<div><Persona {...authorPersona} size={PersonaSize.large} /></div>
|
||||
);
|
||||
} else return (<div className={styles.noPhotoMsg}>{strings.EmptyPhotoMsg}</div>);
|
||||
}
|
||||
} as IColumn);
|
||||
}
|
||||
});
|
||||
setColumns(cols);
|
||||
};
|
||||
/**
|
||||
* People Picker change event
|
||||
*/
|
||||
const _selectedItems = (items: any[]) => {
|
||||
let userInfo: IUserPickerInfo[] = [];
|
||||
if (items && items.length > 0) {
|
||||
items.map(item => {
|
||||
userInfo.push({
|
||||
Title: item.text,
|
||||
LoginName: item.loginName,
|
||||
PhotoUrl: item.imageUrl
|
||||
});
|
||||
});
|
||||
_buildColumns(Object.keys(userInfo[0]));
|
||||
}
|
||||
setSelectedUsers(userInfo);
|
||||
enableButton();
|
||||
hideUpdateButton();
|
||||
};
|
||||
/**
|
||||
* Set the defaultusers property for people picker control, this is used when clearing the data.
|
||||
*/
|
||||
const _getSelectedUsersLoginNames = (items: any[]): string[] => {
|
||||
let retUsers: string[] = [];
|
||||
retUsers = map(items, (o) => { return o.LoginName.split('|')[2]; });
|
||||
return retUsers;
|
||||
};
|
||||
/**
|
||||
* To display the photos from Azure AD
|
||||
*/
|
||||
const _getPhotosFromAzure = async () => {
|
||||
toggleDisableUserPicker();
|
||||
toggleDisableButton();
|
||||
togglePhotoLoader();
|
||||
let res: any[] = await appContext.helper.getUserPhotoFromAADForDisplay(selectedUsers);
|
||||
if (res && res.length > 0) {
|
||||
let tempUsers: IUserPickerInfo[] = selectedUsers;
|
||||
res.map(response => {
|
||||
if (response.responses && response.responses.length > 0) {
|
||||
response.responses.map(finres => {
|
||||
var fil = filter(tempUsers, (o) => { return o.LoginName == finres.id; });
|
||||
if (fil && fil.length > 0) {
|
||||
fil[0].AADPhotoUrl = finres.body.error ? '' : "data:image/jpg;base64," + finres.body;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
setSelectedUsers(tempUsers);
|
||||
_buildColumns(Object.keys(tempUsers[0]));
|
||||
}
|
||||
toggleDisableUserPicker();
|
||||
togglePhotoLoader();
|
||||
toggleShowUpdateButton();
|
||||
setMessage({Message: strings.NoAADPhotos, Scope: MessageScope.Info});
|
||||
};
|
||||
/**
|
||||
* To download the photo thumbnails from Azure to document library.
|
||||
* To send the updated final json to the Azure function to trigger the job for photo sync
|
||||
*/
|
||||
const _syncPhotoToSPUPS = async () => {
|
||||
toggleProcessingPhotoUpdate();
|
||||
let finalUsers: any[] = filter(selectedUsers, (o) => { return o.AADPhotoUrl; });
|
||||
let userVals: IAzFuncValues[] = await appContext.helper.getAndStoreUserThumbnailPhotos(finalUsers, appContext.tempLib);
|
||||
let itemID = await appContext.helper.createSyncItem(SyncType.Manual);
|
||||
await props.updateSPWithPhoto(uniqBy(userVals, 'userid'), itemID);
|
||||
setSelectedUsers([]);
|
||||
toggleProcessingPhotoUpdate();
|
||||
setMessage({ Message: strings.UpdateProcessInitialized, Scope: MessageScope.Success });
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{message && message.Message && message.Message.length > 0 &&
|
||||
<MessageContainer MessageScope={message.Scope} Message={message.Message} />
|
||||
}
|
||||
<PeoplePicker
|
||||
disabled={disableUserPicker || processingPhotoUpdate}
|
||||
context={appContext.context}
|
||||
titleText={strings.PPLPickerTitleText}
|
||||
personSelectionLimit={10}
|
||||
groupName={""} // Leave this blank in case you want to filter from all users
|
||||
showtooltip={false}
|
||||
isRequired={false}
|
||||
showHiddenInUI={false}
|
||||
principalTypes={[PrincipalType.User]}
|
||||
resolveDelay={500}
|
||||
selectedItems={_selectedItems}
|
||||
defaultSelectedUsers={selectedUsers.length > 0 ? _getSelectedUsersLoginNames(selectedUsers) : []}
|
||||
/>
|
||||
{selectedUsers && selectedUsers.length > 0 &&
|
||||
<>
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<PrimaryButton text={strings.BtnAzurePhotoProps} onClick={_getPhotosFromAzure} disabled={disableButton || processingPhotoUpdate} />
|
||||
{showPhotoLoader && <Spinner className={styles.generateTemplateLoader} label={strings.PropsLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
<div style={{ marginTop: '5px' }}>
|
||||
<DetailsList
|
||||
items={selectedUsers}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true} />
|
||||
</div>
|
||||
{showUpdateButton &&
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<PrimaryButton text={strings.BtnUpdatePhoto} onClick={_syncPhotoToSPUPS} disabled={processingPhotoUpdate} />
|
||||
{processingPhotoUpdate && <Spinner className={styles.generateTemplateLoader} label={strings.PropsLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSelectionSync;
|
|
@ -0,0 +1,59 @@
|
|||
define([], function () {
|
||||
return {
|
||||
PropertyPaneDescription: "",
|
||||
BasicGroupName: "Configurations",
|
||||
ListCreationText: "Verifying the required list...",
|
||||
PropTemplateLibLabel: "Select a library to store the templates",
|
||||
PropTempLibLabel: "Select a library to store the thumbnail photos",
|
||||
PropDelThumbnail: "Turn on to delete the thumbnail stored",
|
||||
PropAzFuncLabel: "Azure Function URL",
|
||||
PropAzFuncDesc: "Azure powershell function to update the user profile properties in SharePoint UPS",
|
||||
PropUseCertLabel: "Use Certificate for Azure Function authentication",
|
||||
PropUseCertCallout: "Turn on this option to use certificate for authenticating SharePoint communication via Azure Function",
|
||||
PropDelThumbnailCallout: "Option to delete the thumbnail stored. You can also turn off and use the thumbnail photos for other purpose.",
|
||||
PropDateFormatLabel: "Date format",
|
||||
PropInfoDateFormat: "The date format use <strong>momentjs</strong> date format. Please <a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>click here</a> to get more info on how to define the format. By default the format is '<strong>DD, MMM YYYY hh:mm A</strong>'",
|
||||
PropInfoUseCert: "Please <a href='https://www.youtube.com/watch?v=plS_1BsQAto&list=PL-KKED6SsFo8TxDgQmvMO308p51AO1zln&index=2&t=0s' target='_blank'>click here</a> to see how to create Azure powershell function with different authentication mechanism.",
|
||||
PropInfoTemplateLib: "Document library to maintain the templates and batch files uploaded. </br>'<strong>SyncJobTemplate</strong>' folder will be created to maintain the templates.</br>'<strong>UPSDataToProcess</strong>' folder will be created to maintain the files uploaded for bulk sync.",
|
||||
PropInfoNormalUser: "Sorry, the configuration is enabled only for the site administrators, please contact your site administrator!",
|
||||
PropAllowedUserInfo: "Only SharePoint groups are allowed in this setting. Only memebers of the SharePoint groups defined above will have access to this web part.",
|
||||
PropEnableBUCallout: "Turn on to enable bulk update",
|
||||
PropInfoTempLib: "Document library used to store the user profile thumbnails. You can opt for the automatic removal of thumbnail by turning 'On' the below setting.",
|
||||
|
||||
DefaultAppTitle: "SharePoint Profile Photo Sync",
|
||||
PlaceholderIconText: "Configure the settings",
|
||||
PlaceholderDescription: "Use the configuration settings to map the document library, azure function and other settings.",
|
||||
PlaceholderButtonLabel: "Configure",
|
||||
AccessCheckDesc: "Checking for access...",
|
||||
SitePrivilegeCheckLabel: "Checking site admin privilege...",
|
||||
|
||||
BtnUploadPhotoDataForSync: "Upload Data to Sync",
|
||||
BtnUpdatePhotoProps: "Update User Properties",
|
||||
BtnAzurePhotoProps: "Get Photo from Azure AD",
|
||||
BtnUpdatePhoto: "Update User's Photo",
|
||||
|
||||
PPLPickerTitleText: "Select users to sync their photos!",
|
||||
Photo_UserListChanges: "Changes in user list, please remove the user from the table manually or reinitialize or get the photo again!",
|
||||
Photo_UserListEmpty: "Since all the users have been removed, the table has been cleared!",
|
||||
PropsLoader: "Please wait...",
|
||||
PropsUpdateLoader: "Please wait, initializing the job to update the properties",
|
||||
AdminConfigHelp: "Please contact your site administrator to configure the webpart.",
|
||||
AccessDenied: "Access denied. Please contact your administrator.",
|
||||
NoAADPhotos: "User Photos that are not updated in Azure AD are skipped in the update process.",
|
||||
UpdateProcessInitialized: "Sync Job triggered to update the photos. Track the status on 'Sync Status' tab!",
|
||||
EmptyPhotoMsg: "Photo not found!",
|
||||
BulkSyncNote: "The photo filename should be in the format 'LoginID.png'. Supported image types are '.png', '.jpeg', '.jpg'. Photo(s) with invalid filename will not be processed.",
|
||||
BulkPhotoDragDrop: "Drag 'n' drop the photos, or click to select the photos",
|
||||
JobsListLoaderDesc: "Loading the jobs list...",
|
||||
EmptyTable: "Sorry, no data to be displayed!",
|
||||
JobResultsDialogTitle: "Users list with photo updated!",
|
||||
JobResultsLoaderDesc: "Loading the results...",
|
||||
SyncFailedErrorMessage: "Oops, there is an error while updating the properties. Error Message:",
|
||||
|
||||
TabMenu1: "User Selection Photo Sync",
|
||||
TabMenu2: "Bulk Sync",
|
||||
//TabMenu3: "Bulk Files Uploaded",
|
||||
// TabMenu4: "Templates Generated",
|
||||
TabMenu3: "Sync Status"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
declare interface IPhotoSyncWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
ListCreationText: string;
|
||||
PropTemplateLibLabel: string;
|
||||
PropTempLibLabel: string;
|
||||
PropDelThumbnail: string;
|
||||
PropAzFuncLabel: string;
|
||||
PropAzFuncDesc: string;
|
||||
PropUseCertLabel: string;
|
||||
PropUseCertCallout: string;
|
||||
PropDelThumbnailCallout: string;
|
||||
PropDateFormatLabel: string;
|
||||
PropInfoDateFormat: string;
|
||||
PropInfoUseCert: string;
|
||||
PropInfoTemplateLib: string;
|
||||
PropInfoNormalUser: string;
|
||||
PropAllowedUserInfo: string;
|
||||
PropEnableBUCallout: string;
|
||||
PropInfoTempLib: string;
|
||||
|
||||
DefaultAppTitle: string;
|
||||
PlaceholderIconText: string;
|
||||
PlaceholderDescription: string;
|
||||
PlaceholderButtonLabel: string;
|
||||
AccessCheckDesc: string;
|
||||
SitePrivilegeCheckLabel: string;
|
||||
|
||||
BtnUploadPhotoDataForSync: string;
|
||||
BtnUpdatePhotoProps: string;
|
||||
BtnAzurePhotoProps: string;
|
||||
BtnUpdatePhoto: string;
|
||||
|
||||
PPLPickerTitleText: string;
|
||||
Photo_UserListChanges: string;
|
||||
Photo_UserListEmpty: string;
|
||||
PropsLoader: string;
|
||||
PropsUpdateLoader: string;
|
||||
AdminConfigHelp: string;
|
||||
AccessDenied: string;
|
||||
NoAADPhotos: string;
|
||||
UpdateProcessInitialized: string;
|
||||
EmptyPhotoMsg: string;
|
||||
BulkSyncNote: string;
|
||||
BulkPhotoDragDrop: string;
|
||||
JobsListLoaderDesc: string;
|
||||
EmptyTable: string;
|
||||
JobResultsDialogTitle: string;
|
||||
JobResultsLoaderDesc: string;
|
||||
SyncFailedErrorMessage: string;
|
||||
|
||||
TabMenu1: string;
|
||||
TabMenu2: string;
|
||||
TabMenu3: string;
|
||||
TabMenu4: string;
|
||||
TabMenu5: string;
|
||||
}
|
||||
|
||||
declare module 'PhotoSyncWebPartStrings' {
|
||||
const strings: IPhotoSyncWebPartStrings;
|
||||
export = strings;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue