Initial release of SPUPSProperty Sync
This commit is contained in:
parent
efb1ec22f3
commit
7739e6f934
|
@ -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,33 @@
|
|||
# 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
|
||||
*.scss.d.ts
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.10.0",
|
||||
"libraryName": "react-spupsproperty-sync",
|
||||
"libraryId": "42ba670c-573b-46f9-a529-3990cd2a5bc8",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
# SPUPS Property Sync
|
||||
|
||||
## Summary
|
||||
> This component will help the administrators who are currently maintaining the user profiles in their organization. Since not all the properties from Azure are synced automatically to **SharePoint UPS**, this solution will help administrator to synchronize specific properties (default or custom) from Azure or maintained in a separate system directly to **SharePoint UPS** using property import. Below are the features
|
||||
* **Property Mapping** will provide a flexible and user-friendly way to pick the properties to be synced.
|
||||
* **Templates** can be generated based on the '_Property Mapping_'.
|
||||
* Download the generated templates in both **CSV** & **JSON** format.
|
||||
* **User selection** to allow you to update only the users whose properties are changed or yet to be updated.
|
||||
* **User selection** method will allow the admin to update both
|
||||
* **Manually** entered properties or which are maintained in a separate system
|
||||
* Properties from **Azure AD**
|
||||
* **Bulk Sync** will allow the admin to upload the data using the templates generated. They can also use this templates as a base for exporting the data from other system and then feed them here to update the properties.
|
||||
* **Access control** based on **SharePoint Group**, not all the users can access the applictaion.
|
||||
* **Anytime access** to the template files generated with different property set and the files uploaded for bulk update.
|
||||
* Separate section to check the **status** of the property update. **Detailed status** on each property and also display the overall status.
|
||||
* **Azure Function** to handle the property 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 templates**: A document library to store all the templates generated and also the data files uploaded for bulk sync.
|
||||
|
||||
2. **Azure Function URL**: Azure function URL to run the property update silently.
|
||||
|
||||
3. **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.
|
||||
|
||||
4. **Date format**: Date format to be used across the entire application. Used _**momentJS**_.
|
||||
|
||||
5. **SharePoint Groups**: Only the users from the configured SharePoint Groups and Site Administrator shall be allowed access.
|
||||
|
||||
6. **Use page full width**: This is used when the web part is added to a site page where it has to use full width.
|
||||
|
||||
> **_Note_**: Only the Site Administrator is allowed to update the application properties.
|
||||
|
||||
## Preview
|
||||
![SPUPS-Property-Sync](./assets/SPUPS-Sync.gif)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/version-GA-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.10.0**
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
SPUPS Property Sync | Sudharsan K.([@sudharsank](https://twitter.com/sudharsank), [Know More](http://windowssharepointserver.blogspot.com/))
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0.0|Apr 23 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 below 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)
|
Binary file not shown.
After Width: | Height: | Size: 11 MiB |
|
@ -0,0 +1,85 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
# Read the request as a JSON object
|
||||
$jsoninput = Get-Content $req -Raw | ConvertFrom-Json
|
||||
# Configure local variable
|
||||
$targetSiteUrl = $jsoninput.targetSiteUrl
|
||||
$targetAdminUrl = $jsoninput.targetAdminUrl
|
||||
$usecert = $jsoninput.usecert
|
||||
$itemid = $jsoninput.itemId
|
||||
$targetList = "UPS Sync Jobs"
|
||||
|
||||
$targetSiteUrl
|
||||
|
||||
# Formatted compressed JSON to be stored in list.
|
||||
$dataToSync = $jsoninput | ConvertTo-Json -depth 100 -Compress
|
||||
|
||||
connectSPOnline -targeturl $targetSiteUrl -usecert $usecert
|
||||
Set-PnPListItem -List $targetList -Identity $itemid -Values @{"Status" = "In-Progress" } -SystemUpdate
|
||||
|
||||
connectSPOnline -targeturl $targetAdminUrl -usecert $usecert
|
||||
# Iterate the JSON object and update SharePoint User Profile property
|
||||
foreach ($val in $jsoninput.value) {
|
||||
if ($null -ne $val.properties) {
|
||||
foreach ($prop in $val.properties) {
|
||||
try {
|
||||
if(($null -ne $prop.value) -and ($prop.value -ne "")) {
|
||||
if($prop.name -eq "Department") {
|
||||
Set-PnPUserProfileProperty -Account $val.userid -Property "SPS-Department" -Value $prop.value
|
||||
}
|
||||
if($prop.name -eq "Title"){
|
||||
Set-PnPUserProfileProperty -Account $val.userid -Property "SPS-JobTitle" -Value $prop.value
|
||||
}
|
||||
if($prop.name -eq "Office"){
|
||||
Set-PnPUserProfileProperty -Account $val.userid -Property "SPS-Location" -Value $prop.value
|
||||
}
|
||||
Set-PnPUserProfileProperty -Account $val.userid -Property $prop.name -Value $prop.value
|
||||
if ($null -eq $prop.Status) {
|
||||
$prop | Add-Member -Name "Status" -Value "Updated" -MemberType NoteProperty
|
||||
}
|
||||
else {
|
||||
$prop.Status = "Updated"
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($null -eq $prop.Status) {
|
||||
$prop | Add-Member -Name "Status" -Value "Not Updated" -MemberType NoteProperty
|
||||
}
|
||||
else {
|
||||
$prop.Status = "Not Updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if ($null -eq $prop.Error) {
|
||||
$prop | Add-Member -Name "Error" -Value "An error occurred while updating the property!" -MemberType NoteProperty
|
||||
}
|
||||
else {
|
||||
$prop.Error = "An error occurred while updating the property!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectSPOnline -targeturl $targetSiteUrl -usecert $usecert
|
||||
# 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
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"spups-propery-sync-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/spupsProperySync/SpupsProperySyncWebPart.js",
|
||||
"manifest": "./src/webparts/spupsProperySync/SpupsProperySyncWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"SpupsProperySyncWebPartStrings": "lib/webparts/spupsProperySync/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-spupsproperty-sync",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "React SPUPSproperty Sync",
|
||||
"id": "42ba670c-573b-46f9-a529-3990cd2a5bc8",
|
||||
"title": "SPUPSProperty Sync",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"isDomainIsolated": false,
|
||||
"iconPath": "Images/AppIcon.png",
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "User.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.Read.All"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-spupsproperty-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,8 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
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.addSuppression(`Warning - [sass] The local CSS class 'ms-BasePicker-text' is not camelCase and will not be type-safe.`);
|
||||
|
||||
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "react-spupsproperty-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",
|
||||
"serve": "cross-env NODE_OPTIONS=--max_old_space_size=4096 gulp bundle --custom-serve && cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack-dev-server --mode development --config ./webpack.js --env.env=dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.10.0",
|
||||
"@microsoft/sp-lodash-subset": "1.10.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
|
||||
"@microsoft/sp-property-pane": "1.10.0",
|
||||
"@microsoft/sp-webpart-base": "1.10.0",
|
||||
"@pnp/common": "^2.0.3",
|
||||
"@pnp/graph": "^2.0.3",
|
||||
"@pnp/logging": "^2.0.3",
|
||||
"@pnp/odata": "^2.0.3",
|
||||
"@pnp/sp": "^2.0.3",
|
||||
"@pnp/spfx-controls-react": "^1.16.0",
|
||||
"@pnp/spfx-property-controls": "^1.17.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "16.8.8",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"csvtojson": "^2.0.10",
|
||||
"json2csv": "^5.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"office-ui-fabric-react": "6.189.2",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5",
|
||||
"react-editable-table": "^1.12.32"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||
"@microsoft/sp-build-web": "1.10.0",
|
||||
"@microsoft/sp-module-interfaces": "1.10.0",
|
||||
"@microsoft/sp-tslint-rules": "1.10.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.10.0",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
"cross-env": "7.0.2",
|
||||
"del": "5.1.0",
|
||||
"gulp": "~3.9.1",
|
||||
"jquery": "^3.4.1"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 992 B |
|
@ -0,0 +1,66 @@
|
|||
export enum FileContentType {
|
||||
Blob,
|
||||
Text,
|
||||
ArrayBuffer,
|
||||
JSON
|
||||
}
|
||||
|
||||
export enum MessageScope {
|
||||
Success,
|
||||
Failure,
|
||||
Warning,
|
||||
Info,
|
||||
Blocked,
|
||||
SevereWarning
|
||||
}
|
||||
|
||||
export enum SyncType {
|
||||
Manual = "Manual",
|
||||
Azure = "Azure",
|
||||
Template = "Template"
|
||||
}
|
||||
|
||||
export enum JobStatus {
|
||||
Submitted = "Submitted",
|
||||
InProgress = "In-Progress",
|
||||
Completed = "Completed",
|
||||
CompletedWithError = "Completed With Error",
|
||||
Error = "Error"
|
||||
}
|
||||
|
||||
export interface IUserInfo {
|
||||
ID: number;
|
||||
Email: string;
|
||||
LoginName: string;
|
||||
DisplayName: string;
|
||||
Picture: string;
|
||||
IsSiteAdmin: boolean;
|
||||
Groups: string[];
|
||||
}
|
||||
|
||||
export interface IPropertyMappings {
|
||||
ID: number;
|
||||
Title: string;
|
||||
AzProperty: string;
|
||||
SPProperty: string;
|
||||
IsActive: boolean;
|
||||
AutoSync: boolean;
|
||||
IsIncluded?: boolean;
|
||||
}
|
||||
|
||||
export interface IPropertyPair {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IUserPropertyMapping {
|
||||
UserID: string;
|
||||
Properties: IPropertyPair[];
|
||||
}
|
||||
|
||||
export interface IJsonMapping {
|
||||
targetSiteUrl?: string;
|
||||
targetAdminUrl?: string;
|
||||
values?: IUserPropertyMapping[];
|
||||
}
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
import { HttpClient, IHttpClientOptions, HttpClientResponse } from '@microsoft/sp-http';
|
||||
import { sp } from "@pnp/sp";
|
||||
import "@pnp/sp/webs";
|
||||
import "@pnp/sp/site-users/web";
|
||||
import "@pnp/sp/lists/web";
|
||||
import "@pnp/sp/items/list";
|
||||
import "@pnp/sp/fields/list";
|
||||
import "@pnp/sp/views/list";
|
||||
import "@pnp/sp/profiles";
|
||||
import "@pnp/sp/search";
|
||||
import "@pnp/sp/files";
|
||||
import "@pnp/sp/folders";
|
||||
import { graph } from "@pnp/graph";
|
||||
import "@pnp/graph/users";
|
||||
import * as moment from 'moment/moment';
|
||||
import { IWeb } from "@pnp/sp/webs";
|
||||
import { IUserInfo, IPropertyMappings, IPropertyPair, FileContentType, SyncType, JobStatus } from "./IModel";
|
||||
import { IList } from '@pnp/sp/lists';
|
||||
import { ChoiceFieldFormatType } from '@pnp/sp/fields/types';
|
||||
|
||||
const map: any = require('lodash/map');
|
||||
const intersection: any = require('lodash/intersection');
|
||||
const orderBy: any = require('lodash/orderBy');
|
||||
|
||||
export interface ISPHelper {
|
||||
getCurrentUserInfo: () => Promise<IUserInfo>;
|
||||
checkCurrentUserGroup: (allowedGroups: string[], userGroups: string[]) => boolean;
|
||||
getAzurePropertyForUsers: (selectFields: string, filterQuery: string) => Promise<any[]>;
|
||||
getPropertyMappings: () => Promise<any[]>;
|
||||
getPropertyMappingsTemplate: (propertyMappings: IPropertyMappings[]) => Promise<any>;
|
||||
addFilesToFolder: (filename: string, fileContent: any) => void;
|
||||
getFileContent: (filepath: string, contentType: FileContentType) => void;
|
||||
createSyncItem: (syncType: SyncType) => Promise<number>;
|
||||
updateSyncItem: (itemid: number, inputJson: string) => void;
|
||||
updateSyncItemStatus: (itemid: number, errMsg: string) => void;
|
||||
getAllJobs: () => void;
|
||||
getAllTemplates: () => void;
|
||||
getAllBulkList: () => void;
|
||||
|
||||
runAzFunction: (httpClient: HttpClient, inputData: any, azFuncUrl: string, itemid: number) => void;
|
||||
}
|
||||
|
||||
export default class SPHelper implements ISPHelper {
|
||||
|
||||
private SiteURL: string = "";
|
||||
private SiteRelativeURL: string = "";
|
||||
private AdminSiteURL: string = "";
|
||||
private SyncTemplateFilePath: string = "";
|
||||
private SyncUploadFilePath: string = "";
|
||||
private SyncJSONFileName: string = `SyncTemplate_${moment().format("MMDDYYYYhhmmss")}.json`;
|
||||
private SyncCSVFileName: string = `SyncTemplate_${moment().format("MMDDYYYYhhmmss")}.csv`;
|
||||
private _web: IWeb = null;
|
||||
|
||||
private Lst_PropsMapping = 'Sync Properties Mapping';
|
||||
private Lst_SyncJobs = 'UPS Sync Jobs';
|
||||
|
||||
constructor(siteurl: string, tenantname: string, domainname: string, relativeurl: string, libid: string) {
|
||||
this.SiteURL = siteurl;
|
||||
this.SiteRelativeURL = relativeurl;
|
||||
this.AdminSiteURL = `https://${tenantname}-admin.${domainname}`;
|
||||
this._web = sp.web;
|
||||
this.getTemplateLibraryInfo(libid);
|
||||
}
|
||||
|
||||
public getTemplateLibraryInfo = async (libid: string) => {
|
||||
if (libid) {
|
||||
let libinfo = await this._web.lists.getById(libid).select('Title').get();
|
||||
this.SyncTemplateFilePath = `/${libinfo.Title}/SyncJobTemplate/`;
|
||||
this.SyncUploadFilePath = `/${libinfo.Title}/UPSDataToProcess/`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the Azure property data for the Users
|
||||
*/
|
||||
public getAzurePropertyForUsers = async (selectFields: string, filterQuery: string): Promise<any[]> => {
|
||||
let users = await graph.users.filter(filterQuery).select(selectFields).get();
|
||||
return orderBy(users, 'displayName', 'asc');
|
||||
}
|
||||
/**
|
||||
* Get the property mappings from the 'Sync Properties Mapping' list.
|
||||
*/
|
||||
public getPropertyMappings = async (): Promise<any[]> => {
|
||||
return await this._web.lists.getByTitle(this.Lst_PropsMapping).items
|
||||
.select("ID", "Title", "AzProperty", "SPProperty", "IsActive", "AutoSync")
|
||||
.filter(`IsActive eq 1`)
|
||||
.get();
|
||||
}
|
||||
/**
|
||||
* Generated the property mapping json content.
|
||||
*/
|
||||
public getPropertyMappingsTemplate = async (propertyMappings: IPropertyMappings[]) => {
|
||||
if (!propertyMappings) propertyMappings = await this.getPropertyMappings();
|
||||
let finalJson: string = "";
|
||||
let propertyPair: any[] = [];
|
||||
let sampleUser1 = new Object();
|
||||
let sampleUser2 = new Object();
|
||||
sampleUser1['UserID'] = "user1@tenantname.onmicrosoft.com";
|
||||
sampleUser2['UserID'] = "user2@tenantname.onmicrosoft.com";
|
||||
propertyMappings.map((propsMap: IPropertyMappings) => {
|
||||
sampleUser1[propsMap.SPProperty] = "";
|
||||
sampleUser2[propsMap.SPProperty] = "";
|
||||
});
|
||||
propertyPair.push(sampleUser1, sampleUser2);
|
||||
finalJson = JSON.stringify(propertyPair);
|
||||
return JSON.parse(finalJson);
|
||||
}
|
||||
public getPropertyMappingsTemplate1 = async (propertyMappings: IPropertyMappings[]) => {
|
||||
if (!propertyMappings) propertyMappings = await this.getPropertyMappings();
|
||||
let finalJson: string = "";
|
||||
let propertyPair: IPropertyPair[] = [];
|
||||
propertyMappings.map((propsMap: IPropertyMappings) => {
|
||||
propertyPair.push({
|
||||
name: propsMap.SPProperty,
|
||||
value: ""
|
||||
});
|
||||
});
|
||||
finalJson = `{
|
||||
"targetAdminUrl": "${this.AdminSiteURL}",
|
||||
"targetSiteUrl": "${this.SiteURL}",
|
||||
"values": [
|
||||
{
|
||||
"UserID": "userid@tenantname.onmicrosoft.com",
|
||||
"Properties": ${JSON.stringify(propertyPair)}
|
||||
}
|
||||
]
|
||||
}`;
|
||||
return JSON.parse(finalJson);
|
||||
}
|
||||
/**
|
||||
* Get the file content as blob based on the file url.
|
||||
*/
|
||||
public getFileContent = async (filepath: string, contentType: FileContentType) => {
|
||||
switch (contentType) {
|
||||
case FileContentType.Blob:
|
||||
return await this._web.getFileByServerRelativeUrl(filepath).getBlob();
|
||||
case FileContentType.ArrayBuffer:
|
||||
return await this._web.getFileByServerRelativeUrl(filepath).getBuffer();
|
||||
case FileContentType.Text:
|
||||
return await this._web.getFileByServerRelativeUrl(filepath).getText();
|
||||
case FileContentType.JSON:
|
||||
return await this._web.getFileByServerRelativeUrl(filepath).getJSON();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add the template file to a folder with contents.
|
||||
* This is used for creating the template json file.
|
||||
*/
|
||||
public addFilesToFolder = async (fileContent: any, isCSV: boolean) => {
|
||||
let filename = (isCSV) ? this.SyncCSVFileName : this.SyncJSONFileName;
|
||||
await this.checkAndCreateFolder(this.SiteRelativeURL + this.SyncTemplateFilePath);
|
||||
return await this._web.getFolderByServerRelativeUrl(this.SiteRelativeURL + this.SyncTemplateFilePath)
|
||||
.files
|
||||
.add(decodeURI(this.SiteRelativeURL + this.SyncTemplateFilePath + filename), fileContent, true);
|
||||
}
|
||||
/**
|
||||
* Add the data file to a folder with contents.
|
||||
* This is used for creating the template json file.
|
||||
*/
|
||||
public addDataFilesToFolder = async (fileContent: any, filename: string) => {
|
||||
await this.checkAndCreateFolder(this.SiteRelativeURL + this.SyncUploadFilePath);
|
||||
return await this._web.getFolderByServerRelativeUrl(this.SiteRelativeURL + this.SyncUploadFilePath)
|
||||
.files
|
||||
.add(decodeURI(this.SiteRelativeURL + this.SyncUploadFilePath + filename), fileContent, true);
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get current logged in user information.
|
||||
*/
|
||||
public getCurrentUserInfo = 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;
|
||||
}
|
||||
/**
|
||||
* 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 () => {
|
||||
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();
|
||||
}
|
||||
/**
|
||||
* Get all the templates generated
|
||||
*/
|
||||
public getAllTemplates = async () => {
|
||||
return await this._web.getFolderByServerRelativeUrl(this.SiteRelativeURL + this.SyncTemplateFilePath)
|
||||
.files
|
||||
.select('Name', 'ServerRelativeUrl', 'TimeCreated')
|
||||
.expand('Author')
|
||||
.get();
|
||||
}
|
||||
/**
|
||||
* Get all the bulk sync files
|
||||
*/
|
||||
public getAllBulkList = async () => {
|
||||
return await this._web.getFolderByServerRelativeUrl(this.SiteRelativeURL + this.SyncUploadFilePath)
|
||||
.files
|
||||
.select('Name', 'ServerRelativeUrl', 'TimeCreated')
|
||||
.expand('Author')
|
||||
.get();
|
||||
}
|
||||
/**
|
||||
* 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_PropsMapping).get();
|
||||
console.log('Property Mapping List Exists');
|
||||
} catch (err) {
|
||||
console.log("Property Mapping List doesn't exists, so creating");
|
||||
await this._createPropsMappingList();
|
||||
console.log("Property Mapping List created");
|
||||
}
|
||||
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.addMultilineText('ErrorMessage', 6, false, false, false, false, {Required: false, Description: 'Store the error message while calling Azure function.'});
|
||||
await listExists.fields.addChoice('SyncType', ['Manual', 'Azure', 'Template'], ChoiceFieldFormatType.Dropdown, false, { Required: true, Description: 'Type of data sent to 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('SyncData');
|
||||
allItemsView.fields.inBatch(batch).add('SyncedData');
|
||||
allItemsView.fields.inBatch(batch).add('Status');
|
||||
allItemsView.fields.inBatch(batch).add('ErrorMessage');
|
||||
allItemsView.fields.inBatch(batch).add('SyncType');
|
||||
allItemsView.fields.inBatch(batch).move('ID', 0);
|
||||
await batch.execute();
|
||||
}
|
||||
/**
|
||||
* Create property mapping list
|
||||
*/
|
||||
public _createPropsMappingList = async () => {
|
||||
let listExists = await (await sp.web.lists.ensure(this.Lst_PropsMapping)).list;
|
||||
await listExists.fields.addText('AzProperty', 255, { Required: true, Description: 'Azure user profile property name.' });
|
||||
await listExists.fields.addText('SPProperty', 255, { Required: true, Description: 'SharePoint User Profile property name.' });
|
||||
await listExists.fields.addBoolean('IsActive', { Required: true, Description: 'Active or InActive used for mapping by the end users.' });
|
||||
await listExists.fields.addBoolean('AutoSync', { Required: true, Description: 'Properties that are automatically synced with Azure.' });
|
||||
let allItemsView = await listExists.views.getByTitle('All Items');
|
||||
let batch = sp.createBatch();
|
||||
allItemsView.fields.inBatch(batch).add('AzProperty');
|
||||
allItemsView.fields.inBatch(batch).add('SPProperty');
|
||||
allItemsView.fields.inBatch(batch).add('IsActive');
|
||||
allItemsView.fields.inBatch(batch).add('AutoSync');
|
||||
await batch.execute();
|
||||
await this._createDefaultPropsMapping(listExists);
|
||||
}
|
||||
/**
|
||||
* Create default property mapping items
|
||||
*/
|
||||
public _createDefaultPropsMapping = async (lst: IList) => {
|
||||
let batch = sp.createBatch();
|
||||
lst.items.inBatch(batch).add({ Title: 'Department', AzProperty: 'department', SPProperty: 'Department', IsActive: true, AutoSync: true });
|
||||
lst.items.inBatch(batch).add({ Title: 'Job Title', AzProperty: 'jobTitle', SPProperty: 'Title', IsActive: true, AutoSync: true });
|
||||
lst.items.inBatch(batch).add({ Title: 'Office', AzProperty: 'officeLocation', SPProperty: 'Office', IsActive: true, AutoSync: true });
|
||||
lst.items.inBatch(batch).add({ Title: 'Business Phone', AzProperty: 'businessPhones', SPProperty: 'workPhone', IsActive: true, AutoSync: false });
|
||||
lst.items.inBatch(batch).add({ Title: 'Mobile Phone', AzProperty: 'mobilePhone', SPProperty: 'CellPhone', IsActive: true, AutoSync: false });
|
||||
lst.items.inBatch(batch).add({ Title: 'Fax Number', AzProperty: 'faxNumber', SPProperty: 'Fax', IsActive: true, AutoSync: false });
|
||||
lst.items.inBatch(batch).add({ Title: 'Street Address', AzProperty: 'streetAddress', SPProperty: 'StreetAddress', IsActive: true, AutoSync: false });
|
||||
lst.items.inBatch(batch).add({ Title: 'City', AzProperty: 'city', SPProperty: 'City', IsActive: true, AutoSync: false });
|
||||
lst.items.inBatch(batch).add({ Title: 'State or Province', AzProperty: 'state', SPProperty: 'State', IsActive: true, AutoSync: false });
|
||||
lst.items.inBatch(batch).add({ Title: 'Zip or Postal code', AzProperty: 'postalCode', SPProperty: 'PostalCode', IsActive: true, AutoSync: false });
|
||||
lst.items.inBatch(batch).add({ Title: 'Country or Region', AzProperty: 'country', SPProperty: 'Country', IsActive: true, AutoSync: false });
|
||||
await batch.execute();
|
||||
}
|
||||
/**
|
||||
* Azure function to update the UPS 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");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
$ms-greenLight: "[theme:greenLight, default:#bad80a]";
|
||||
$ms-neutralSecondaryAlt: "[theme:info, default:#767676]";
|
||||
$ms-neutralLight: "[theme:infoBackground, default:#eaeaea]";
|
||||
$ms-magenta: "[theme:magenta, default:#b4009e]";
|
||||
$ms-magentaDark: "[theme:magentaDark, default:#5c005c]";
|
||||
$ms-magentaLight: "[theme:magentaLight, default:#e3008c]";
|
||||
$ms-neutralDark: "[theme:neutralDark, default:#212121]";
|
||||
$ms-neutralLight: "[theme:neutralLight, default:#eaeaea]";
|
||||
$ms-neutralLighter: "[theme:neutralLighter, default:#f4f4f4]";
|
||||
$ms-neutralLighterAlt: "[theme:neutralLighterAlt, default:#f8f8f8]";
|
||||
$ms-neutralPrimary: "[theme:neutralPrimary, default:#333333]";
|
||||
$ms-neutralPrimaryAlt: "[theme:neutralPrimaryAlt, default:#3C3C3C]";
|
||||
$ms-neutralQuaternary: "[theme:neutralPrimaryTranslucent50, default:#d0d0d0]";
|
||||
$ms-neutralQuaternaryAlt: "[theme:neutralQuaternary, default:#dadada]";
|
||||
$ms-neutralSecondary: "[theme:neutralQuaternaryAlt, default:#666666]";
|
||||
$ms-neutralSecondaryAlt: "[theme:neutralSecondary, default:#767676]";
|
||||
$ms-neutralTertiary: "[theme:neutralSecondaryAlt, default:#a6a6a6]";
|
||||
$ms-neutralTertiaryAlt: "[theme:neutralTertiary, default:#c8c8c8]";
|
||||
$ms-white: "[theme:neutralTertiaryAlt, default:#ffffff]";
|
||||
$ms-orange: "[theme:orange, default:#d83b01]";
|
||||
$ms-orangeLight: "[theme:orangeLight, default:#ea4300]";
|
||||
$ms-orangeLighter: "[theme:orangeLighter, default:#ff8c00]";
|
||||
$ms-primaryBackground: "[theme:primaryBackground, default:#0078d7]";
|
||||
$ms-primaryText: "[theme:primaryText, default:#0078d7]";
|
||||
$ms-purple: "[theme:purple, default:#5c2d91]";
|
||||
$ms-purpleDark: "[theme:purpleDark, default:#32145a]";
|
||||
$ms-purpleLight: "[theme:purpleLight, default:#b4a0ff]";
|
||||
$ms-red: "[theme:red, default:#e81123]";
|
||||
$ms-redDark: "[theme:redDark, default:#a80000]";
|
||||
$ms-success: "[theme:success, default:#107c10]";
|
||||
$ms-successBackground: "[theme:successBackground, default:#dff6dd]";
|
||||
$ms-teal: "[theme:teal, default:#008272]";
|
||||
$ms-tealDark: "[theme:tealDark, default:#004b50]";
|
||||
$ms-tealLight: "[theme:tealLight, default:#00b294]";
|
||||
$ms-themeAccent: "[theme:themeAccent, default:inherit]";
|
||||
$ms-themeAccentTranslucent10: "[theme:themeAccentTranslucent10, default:inherit]";
|
||||
$ms-themeDark: "[theme:themeDark, default:#005a9e]";
|
||||
$ms-themeDarkAlt: "[theme:themeDarkAlt, default:#106ebe]";
|
||||
$ms-themeDarker: "[theme:themeDarker, default:#004578]";
|
||||
$ms-themeLight: "[theme:themeLight, default:#b3d6f2]";
|
||||
$ms-themeLightAlt: "[theme:themeLightAlt, default:inherit]";
|
||||
$ms-themeLighter: "[theme:themeLighter]";
|
||||
$ms-themeLighterAlt: "[theme:themeLighterAlt, default:#eff6fc]";
|
||||
$ms-themePrimary: "[theme:themePrimary]";
|
||||
$ms-themeSecondary: "[theme:themeSecondary]";
|
||||
$ms-themeTertiary: "[theme:themeTertiary]";
|
||||
$ms-themeTertiaryAlt: "[theme:themeTertiaryAlt, default:#c8c8c8]";
|
||||
$ms-white: "[theme:white, default:#ffffff]";
|
||||
$ms-whiteTranslucent40: "[theme:whiteTranslucent40, default:rgba(255,255,255,.4)]";
|
||||
$ms-yellow: "[theme:yellow, default:#ffb900]";
|
||||
$ms-yellowLight: "[theme:yellowLight, default:#fff100]";
|
||||
$ms-error: "[theme:error]";
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "f137ab5e-5484-4c5e-9344-01eb54f065ec",
|
||||
"alias": "SpupsProperySyncWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart", "SharePointFullPage"],
|
||||
"supportsFullBleed": true,
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "SPUPSPropery Sync" },
|
||||
"description": { "default": "Sync Azure AD properties or User defined values (custom properties) to SharePoint User Profile Properties." },
|
||||
"officeFabricIconFontName": "UserSync",
|
||||
"properties": {}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version, Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
IPropertyPanePage
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart, WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { CalloutTriggers } from '@pnp/spfx-property-controls/lib/PropertyFieldHeader';
|
||||
import { PropertyFieldLabelWithCallout } from '@pnp/spfx-property-controls/lib/PropertyFieldLabelWithCallout';
|
||||
import { PropertyFieldToggleWithCallout } from '@pnp/spfx-property-controls/lib/PropertyFieldToggleWithCallout';
|
||||
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
|
||||
import { PropertyPaneWebPartInformation } from '@pnp/spfx-property-controls/lib/PropertyPaneWebPartInformation';
|
||||
import { PropertyFieldPeoplePicker, PrincipalType, IPropertyFieldGroupOrPerson } from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
|
||||
import { sp } from '@pnp/sp';
|
||||
import { graph } from "@pnp/graph";
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
import SpupsProperySync from './components/SpupsProperySync';
|
||||
import { ISpupsProperySyncProps } from './components/SpupsProperySync';
|
||||
import SPHelper from '../../Common/SPHelper';
|
||||
import { IUserInfo } from '../../Common/IModel';
|
||||
|
||||
export interface ISpupsProperySyncWebPartProps {
|
||||
context: WebPartContext;
|
||||
templateLib: string;
|
||||
appTitle: string;
|
||||
AzFuncUrl: string;
|
||||
UseCert: boolean;
|
||||
dateFormat: string;
|
||||
toggleInfoHeaderValue: boolean;
|
||||
useFullWidth: boolean;
|
||||
allowedUsers: IPropertyFieldGroupOrPerson[];
|
||||
}
|
||||
|
||||
export default class SpupsProperySyncWebPart extends BaseClientSideWebPart<ISpupsProperySyncWebPartProps> {
|
||||
private loadingIndicator: boolean = true;
|
||||
private wpPropertyPages: IPropertyPanePage[] = [];
|
||||
|
||||
protected async onInit() {
|
||||
await super.onInit();
|
||||
sp.setup(this.context);
|
||||
graph.setup({ spfxContext: this.context });
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<ISpupsProperySyncProps> = React.createElement(
|
||||
SpupsProperySync,
|
||||
{
|
||||
context: this.context,
|
||||
templateLib: this.properties.templateLib,
|
||||
displayMode: this.displayMode,
|
||||
appTitle: this.properties.appTitle,
|
||||
AzFuncUrl: this.properties.AzFuncUrl,
|
||||
UseCert: this.properties.UseCert,
|
||||
dateFormat: this.properties.dateFormat ? this.properties.dateFormat : "DD, MMM YYYY hh:mm A",
|
||||
allowedUsers: this.properties.allowedUsers,
|
||||
useFullWidth: this.properties.useFullWidth,
|
||||
updateProperty: (value: string) => {
|
||||
this.properties.appTitle = value;
|
||||
},
|
||||
openPropertyPane: this.openPropertyPane
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
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('templateLib', {
|
||||
key: 'templateLibFieldId',
|
||||
label: strings.PropTemplateLibLabel,
|
||||
selectedList: this.properties.templateLib,
|
||||
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.PropInfoTemplateLib}`,
|
||||
key: 'templateLibInfoId'
|
||||
}),
|
||||
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('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 helper = new SPHelper(this.context.pageContext.legacyPageContext.siteAbsoluteUrl,
|
||||
this.context.pageContext.legacyPageContext.tenantDisplayName,
|
||||
this.context.pageContext.legacyPageContext.webDomain,
|
||||
this.context.pageContext.web.serverRelativeUrl,
|
||||
''
|
||||
);
|
||||
let currentUserInfo: IUserInfo = await helper.getCurrentUserInfo();
|
||||
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,67 @@
|
|||
import * as React from 'react';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
import styles from './SpupsProperySync.module.scss';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { MessageScope } from '../../../Common/IModel';
|
||||
import EditableTable from './DynamicTable/EditableTable';
|
||||
import MessageContainer from './MessageContainer';
|
||||
|
||||
export interface IAzurePropertyViewProps {
|
||||
userProperties: any;
|
||||
showProgress: boolean;
|
||||
UpdateSPUserWithAzureProps: (data: any) => void;
|
||||
}
|
||||
|
||||
export interface IAzurePropertyViewState {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export default class AzurePropertyView extends React.Component<IAzurePropertyViewProps, IAzurePropertyViewState> {
|
||||
constructor(props: IAzurePropertyViewProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: []
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount = async () => {
|
||||
this.setState({ data: this.props.userProperties });
|
||||
}
|
||||
|
||||
public componentDidUpdate = (prevProps: IAzurePropertyViewProps) => {
|
||||
if (prevProps.userProperties !== this.props.userProperties) {
|
||||
this.setState({ data: this.props.userProperties });
|
||||
}
|
||||
}
|
||||
|
||||
private handleRowDel = (item) => {
|
||||
var index = this.state.data.indexOf(item);
|
||||
this.state.data.splice(index, 1);
|
||||
this.setState(this.state.data);
|
||||
}
|
||||
|
||||
private updateWithAzureProperty = () => {
|
||||
this.props.UpdateSPUserWithAzureProps(this.state.data);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { data } = this.state;
|
||||
return (
|
||||
<div>
|
||||
{(data && data.length > 0) ? (
|
||||
<>
|
||||
<EditableTable onRowDel={this.handleRowDel.bind(this)} data={data} isReadOnly={true} />
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<PrimaryButton text={strings.BtnUpdateUserProps} onClick={this.updateWithAzureProperty} style={{ marginRight: '5px' }} disabled={this.props.showProgress} />
|
||||
{this.props.showProgress && <Spinner className={styles.generateTemplateLoader} label={strings.PropsUpdateLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div><MessageContainer MessageScope={MessageScope.Info} Message={strings.EmptyTable} /></div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
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 { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import * as csv from 'csvtojson';
|
||||
import SPHelper from '../../../../Common/SPHelper';
|
||||
import { FileContentType } from '../../../../Common/IModel';
|
||||
|
||||
export interface IBulkSyncDataProps {
|
||||
helper: SPHelper;
|
||||
fileurl: string;
|
||||
}
|
||||
|
||||
export default function BulkSyncData(props: IBulkSyncDataProps) {
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [filedata, setFileData] = React.useState<any[]>([]);
|
||||
const [columns, setColumns] = React.useState<IColumn[]>([]);
|
||||
|
||||
const _buildColumns = (colValues: string[]) => {
|
||||
let cols: IColumn[] = [];
|
||||
colValues.map(col => {
|
||||
if (col.toLowerCase() == "userid") {
|
||||
cols.push({
|
||||
key: 'UserID', name: 'User ID', fieldName: col, minWidth: 250, maxWidth: 250,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: `/_layouts/15/userphoto.aspx?Size=S&Username=${item[col]}`,
|
||||
text: item[col],
|
||||
};
|
||||
return (
|
||||
<div><Persona {...authorPersona} size={PersonaSize.size24} /></div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
} else {
|
||||
cols.push({
|
||||
key: col, name: col, fieldName: col, minWidth: 150,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
if (item[col]) {
|
||||
return (<div>{item[col]}</div>);
|
||||
} else {
|
||||
return (<div className={styles.emptyData}>{strings.EmptyDataText}</div>);
|
||||
}
|
||||
}
|
||||
} as IColumn);
|
||||
}
|
||||
});
|
||||
setColumns(cols);
|
||||
};
|
||||
const _getJSONData = (inputjson?: any) => {
|
||||
let parsedJson = (inputjson) ? inputjson : JSON.parse(inputjson);
|
||||
let _dynamicColumns: string[] = [];
|
||||
Object.keys(parsedJson[0]).map((key) => {
|
||||
_dynamicColumns.push(key);
|
||||
});
|
||||
_buildColumns(_dynamicColumns);
|
||||
setFileData(parsedJson);
|
||||
setLoading(false);
|
||||
};
|
||||
const _buildBulkSyncDataList = async () => {
|
||||
if (props.fileurl) {
|
||||
let fileextn: string = props.fileurl.split('.').pop();
|
||||
let filecontent: any = null;
|
||||
if (fileextn.toLowerCase() === "csv") {
|
||||
filecontent = await props.helper.getFileContent(props.fileurl, FileContentType.Text);
|
||||
let finalOut: any = await csv().fromString(filecontent);
|
||||
_getJSONData(finalOut);
|
||||
}
|
||||
else {
|
||||
filecontent = await props.helper.getFileContent(props.fileurl, FileContentType.JSON);
|
||||
_getJSONData(filecontent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
_buildBulkSyncDataList();
|
||||
}, [props.fileurl]);
|
||||
|
||||
return (
|
||||
<div style={{ maxHeight: '600', maxWidth: '600', overflow: 'auto' }}>
|
||||
{loading &&
|
||||
<Spinner size={SpinnerSize.small} label={strings.BulkSyncFileDataLoaderDesc} labelPosition={"top"} />
|
||||
}
|
||||
{!loading && filedata && filedata.length > 0 &&
|
||||
<DetailsList
|
||||
items={filedata}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true}
|
||||
className={styles.uppropertylist} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
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 { 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 { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { Link } from 'office-ui-fabric-react/lib/Link';
|
||||
import { Icon, IconType, IIconProps } from 'office-ui-fabric-react/lib/Icon';
|
||||
import * as moment from 'moment/moment';
|
||||
import { MessageScope } from '../../../../Common/IModel';
|
||||
import SPHelper from '../../../../Common/SPHelper';
|
||||
import MessageContainer from '../MessageContainer';
|
||||
import BulkSyncData from './BulkSyncData';
|
||||
|
||||
const orderBy: any = require('lodash/orderBy');
|
||||
const filter: any = require('lodash/filter');
|
||||
|
||||
export interface IBulkSyncListProps {
|
||||
helper: SPHelper;
|
||||
siteurl: string;
|
||||
dateFormat: string;
|
||||
}
|
||||
|
||||
export default function BulkSyncList(props: IBulkSyncListProps) {
|
||||
const actionIcon: IIconProps = { iconName: 'InfoSolid' };
|
||||
const refreshIcon: IIconProps = { iconName: 'Refresh' };
|
||||
|
||||
const [refreshLoading, setRefreshLoading] = React.useState<boolean>(false);
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [downloadLoading, setDownloadLoading] = React.useState<boolean>(false);
|
||||
const [bulklist, setBulkList] = React.useState<any[]>([]);
|
||||
const [filteredBulkList, setFilteredBulkList] = React.useState<any[]>([]);
|
||||
const [columns, setColumns] = React.useState<IColumn[]>([]);
|
||||
const [searchKey, setSearchKey] = React.useState<string>('');
|
||||
const [emptySearch, setEmptySearch] = React.useState<boolean>(false);
|
||||
const [hideDialog, setHideDialog] = React.useState<boolean>(true);
|
||||
const [fileurl, setFileUrl] = React.useState<string>('');
|
||||
|
||||
const downloadTemplate = async (fileserverurl, filename) => {
|
||||
setDownloadLoading(true);
|
||||
const anchor = window.document.createElement('a');
|
||||
anchor.href = `${props.siteurl}/_layouts/15/download.aspx?SourceUrl=${fileserverurl}`;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
setDownloadLoading(false);
|
||||
};
|
||||
const actionClick = async (data) => {
|
||||
if (data.FileUrl) {
|
||||
setFileUrl(data.FileUrl);
|
||||
setHideDialog(false);
|
||||
}
|
||||
};
|
||||
const ActionRender = (actionProps) => {
|
||||
return (
|
||||
<ActionButton iconProps={actionIcon} allowDisabledFocus onClick={() => { actionClick(actionProps); }} />
|
||||
);
|
||||
};
|
||||
const _buildColumns = () => {
|
||||
let cols: IColumn[] = [];
|
||||
cols.push({
|
||||
key: 'Name', name: 'Name', fieldName: 'Name', minWidth: 300, maxWidth: 300,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
let fileextn = item.Name.split('.').pop();
|
||||
return (
|
||||
<div style={{display: 'flex', overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
<div className={styles.fileiconDiv}>
|
||||
{fileextn.toLowerCase() === "csv" &&
|
||||
<Icon iconName="ExcelDocument" ariaLabel={item.Name} iconType={IconType.Default} />
|
||||
}
|
||||
{fileextn.toLowerCase() === "json" &&
|
||||
<Icon iconName="FileCode" ariaLabel={item.Name} iconType={IconType.Default} />
|
||||
}
|
||||
</div>
|
||||
<Link onClick={() => { downloadTemplate(`${item.ServerRelativeUrl}`, `${item.Name}`); }} value={item.Name}>{item.Name}</Link>
|
||||
{downloadLoading &&
|
||||
<div className={styles.downloadLoaderDiv}>
|
||||
<Spinner size={SpinnerSize.small} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
cols.push({
|
||||
key: 'Author', name: 'Author', fieldName: 'Author', minWidth: 300, maxWidth: 300,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: `/_layouts/15/userphoto.aspx?Size=S&Username=${item["Author"].Email}`,
|
||||
text: item["Author"].Title,
|
||||
};
|
||||
return (
|
||||
<div><Persona {...authorPersona} size={PersonaSize.size24} /></div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'TimeCreated', name: 'Created', fieldName: 'TimeCreated', minWidth: 100, maxWidth: 200,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (
|
||||
<div>{moment(item.TimeCreated).format(props.dateFormat)}</div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Actions', name: 'Actions', fieldName: 'ID', minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (<ActionRender FileUrl={item.ServerRelativeUrl} />);
|
||||
}
|
||||
});
|
||||
setColumns(cols);
|
||||
};
|
||||
const _loadBulkSyncList = async () => {
|
||||
let bulkSyncList = await props.helper.getAllBulkList();
|
||||
bulkSyncList = orderBy(bulkSyncList, ['TimeCreated'], ['desc']);
|
||||
setBulkList(bulkSyncList);
|
||||
};
|
||||
const _buildBulkSyncList = async () => {
|
||||
_buildColumns();
|
||||
await _loadBulkSyncList();
|
||||
setLoading(false);
|
||||
};
|
||||
const _refreshList = async () => {
|
||||
setRefreshLoading(true);
|
||||
await _loadBulkSyncList();
|
||||
setRefreshLoading(false);
|
||||
};
|
||||
const _searchBulkSyncList = (srchkey) => {
|
||||
setEmptySearch(false);
|
||||
setSearchKey(srchkey);
|
||||
let filteredList = filter(bulklist, (o) => {
|
||||
return o.Name.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0 || o['Author'].Title.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0;
|
||||
});
|
||||
if (filteredList.length <= 0) setEmptySearch(true);
|
||||
setFilteredBulkList(filteredList);
|
||||
};
|
||||
const _closeDialog = () => {
|
||||
setHideDialog(true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
_buildBulkSyncList();
|
||||
}, [props.dateFormat]);
|
||||
|
||||
return (
|
||||
<div className={styles.syncjobsContainer}>
|
||||
{loading &&
|
||||
<ProgressIndicator label={strings.PropsLoader} description={strings.BulkSyncListLoaderDesc} />
|
||||
}
|
||||
{(!loading && bulklist && bulklist.length > 0) ? (
|
||||
<>
|
||||
<div className={styles.searchcontainer}>
|
||||
<SearchBox placeholder={strings.TemplateListSearchPH} underlined={true} value={searchKey} onChange={_searchBulkSyncList} />
|
||||
</div>
|
||||
<div className={styles.refreshContainer}>
|
||||
<IconButton iconProps={refreshIcon} title="Refresh" ariaLabel="Refresh" onClick={_refreshList} disabled={refreshLoading} />
|
||||
{refreshLoading &&
|
||||
<Spinner size={SpinnerSize.small} />
|
||||
}
|
||||
</div>
|
||||
{emptySearch &&
|
||||
<div style={{ marginTop: '-10px', width: '95%' }}>
|
||||
<MessageContainer MessageScope={MessageScope.Failure} Message={strings.EmptySearchResults} />
|
||||
</div>
|
||||
}
|
||||
<div className={styles.templateList}>
|
||||
<DetailsList
|
||||
items={filteredBulkList && filteredBulkList.length > 0 ? filteredBulkList : bulklist}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true}
|
||||
className={styles.uppropertylist} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!loading &&
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.EmptyTable} />
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Dialog hidden={hideDialog} onDismiss={_closeDialog} maxWidth='700' minWidth='500px'
|
||||
dialogContentProps={{
|
||||
type: DialogType.close,
|
||||
title: `${strings.BulkSyncDataDialogTitle}`
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
isDarkOverlay: true,
|
||||
styles: { main: { minWidth: 500, maxHeight: 700, minHeight: 100 } },
|
||||
}}>
|
||||
<BulkSyncData helper={props.helper} fileurl={fileurl} />
|
||||
</Dialog>
|
||||
</div >
|
||||
);
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import * as React from 'react';
|
||||
import { IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import styles from './DynamicTable.module.scss';
|
||||
import EditableCell from './EditableCell';
|
||||
|
||||
export interface IDataRowProps {
|
||||
item: any;
|
||||
columns: any;
|
||||
isReadOnly?: boolean;
|
||||
onTableUpdate: () => void;
|
||||
onDelRow: (item: any) => void;
|
||||
}
|
||||
|
||||
export default function DataRow(props: IDataRowProps) {
|
||||
function onDelEvent() {
|
||||
props.onDelRow(props.item);
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
{props.isReadOnly ? (
|
||||
<>
|
||||
{props.columns.map(col => {
|
||||
if (col.toLocaleLowerCase() !== "imageurl" && col.toLocaleLowerCase() !== "userprincipalname" && col.toLocaleLowerCase() !== "id") {
|
||||
if (col.toLocaleLowerCase() == "displayname") {
|
||||
return <EditableCell cellData={{
|
||||
"type": col,
|
||||
value: props.item.displayName,
|
||||
id: props.item.userPrincipalName,
|
||||
label: true,
|
||||
ImageUrl: props.item.ImageUrl
|
||||
}} isReadOnly={props.isReadOnly} />;
|
||||
|
||||
} else {
|
||||
return <EditableCell cellData={{
|
||||
"type": col,
|
||||
value: props.item[col],
|
||||
id: props.item.userPrincipalName,
|
||||
}} isReadOnly={props.isReadOnly} />;
|
||||
}
|
||||
}
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{props.columns.map(col => {
|
||||
if (col.toLocaleLowerCase() !== "imageurl" && col.toLocaleLowerCase() !== "displayname") {
|
||||
if (col == "UserID") {
|
||||
return <EditableCell onTableUpdate={props.onTableUpdate} cellData={{
|
||||
"type": col,
|
||||
value: props.item.DisplayName,
|
||||
id: props.item.UserID,
|
||||
label: true,
|
||||
ImageUrl: props.item.ImageUrl
|
||||
}} />;
|
||||
} else {
|
||||
return <EditableCell onTableUpdate={props.onTableUpdate} cellData={{
|
||||
"type": col,
|
||||
value: props.item[col],
|
||||
id: props.item.UserID,
|
||||
label: false
|
||||
}} />;
|
||||
}
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<td>
|
||||
<IconButton iconProps={{ iconName: "UserRemove" }} title="Remove" ariaLabel="Remove" onClick={onDelEvent} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
.dynamicTable {
|
||||
overflow: auto;
|
||||
.table {
|
||||
margin-top: 10px;
|
||||
border: 1px solid #CCC;
|
||||
width: 100%;
|
||||
thead > tr > th {
|
||||
padding: 5px;
|
||||
background-color: lightgray;
|
||||
}
|
||||
td:first-child {
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
.textInput {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 1px 15px 0px #ccc;
|
||||
width: 93%;
|
||||
}
|
||||
.divusername {
|
||||
width: 100%;
|
||||
img {
|
||||
width: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
label {
|
||||
padding-left: 3px;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
}
|
||||
span {
|
||||
// white-space: nowrap;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import styles from './DynamicTable.module.scss';
|
||||
|
||||
export interface IEditableCellProps {
|
||||
cellData: any;
|
||||
onTableUpdate?: (event: any) => void;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface IEditableCellState {
|
||||
inputtext: string;
|
||||
}
|
||||
|
||||
export default class EditableCell extends React.Component<IEditableCellProps, IEditableCellState> {
|
||||
constructor(props: IEditableCellProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
inputtext: ""
|
||||
};
|
||||
}
|
||||
|
||||
private handleTextChange = (e) => {
|
||||
this.setState({ inputtext: e.target.value });
|
||||
this.props.onTableUpdate(e);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { cellData, isReadOnly } = this.props;
|
||||
return (
|
||||
<td>
|
||||
{isReadOnly ? (
|
||||
<>
|
||||
{!cellData.label ? (
|
||||
<div>{cellData.value ? cellData.value : " - "}</div>
|
||||
) : (
|
||||
<div className={styles.divusername}>
|
||||
<img src={`/_layouts/15/userphoto.aspx?accountname=${cellData.id}&size=M`} />
|
||||
<label>{cellData.value}</label>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!cellData.label ? (
|
||||
<input type='text' className={styles.textInput} name={cellData.type} id={cellData.id} value={this.state.inputtext} onChange={this.handleTextChange} />
|
||||
) : (
|
||||
<div className={styles.divusername}>
|
||||
<img src={cellData.ImageUrl} />
|
||||
<label>{cellData.value}</label>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import * as React from 'react';
|
||||
import styles from './DynamicTable.module.scss';
|
||||
import DataRow from './DataRow';
|
||||
|
||||
export interface IEditableTableProps {
|
||||
onTableUpdate?: () => void;
|
||||
onRowDel: () => void;
|
||||
filterText?: string;
|
||||
data: any[];
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function EditableTable(props: IEditableTableProps) {
|
||||
var keyData = JSON.parse(JSON.stringify(props.data));
|
||||
var columns = Object.keys(keyData[0]);
|
||||
var rowitem: any = props.data.map((item) => {
|
||||
return (<DataRow item={item} columns={columns} onTableUpdate={props.onTableUpdate} onDelRow={props.onRowDel} key={item.UserID} isReadOnly={props.isReadOnly} />);
|
||||
});
|
||||
return (
|
||||
<div className={styles.dynamicTable}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
{props.isReadOnly ? (
|
||||
<>
|
||||
{columns.map(key => {
|
||||
if (key.toLocaleLowerCase() !== "imageurl" && key.toLocaleLowerCase() !== "userprincipalname" && key.toLocaleLowerCase() !== "id") {
|
||||
return (<th>{key}</th>);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{columns.map(key => {
|
||||
if (key.toLocaleLowerCase() !== "imageurl" && key.toLocaleLowerCase() !== "displayname") {
|
||||
return (<th>{key}</th>);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rowitem}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import * as React from 'react';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
import styles from './SpupsProperySync.module.scss';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import EditableTable from './DynamicTable/EditableTable';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import { MessageScope } from '../../../Common/IModel';
|
||||
|
||||
export interface IManualPropertyUpdateProps {
|
||||
userProperties: any;
|
||||
showProgress: boolean;
|
||||
UpdateSPUserWithManualProps: (data: any) => void;
|
||||
}
|
||||
|
||||
export interface IManualPropertyUpdateState {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export default class ManualPropertyUpdate extends React.Component<IManualPropertyUpdateProps, IManualPropertyUpdateState> {
|
||||
constructor(props: IManualPropertyUpdateProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: []
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount = async () => {
|
||||
this.setState({ data: this.props.userProperties });
|
||||
}
|
||||
|
||||
public componentDidUpdate = (prevProps: IManualPropertyUpdateProps) => {
|
||||
if (prevProps.userProperties !== this.props.userProperties) {
|
||||
this.setState({ data: this.props.userProperties });
|
||||
}
|
||||
}
|
||||
|
||||
private handleRowDel = (item) => {
|
||||
var index = this.state.data.indexOf(item);
|
||||
this.state.data.splice(index, 1);
|
||||
this.setState(this.state.data);
|
||||
}
|
||||
|
||||
private handlePropertyTable = (evt) => {
|
||||
var newProp = {
|
||||
id: evt.target.id,
|
||||
name: evt.target.name,
|
||||
value: evt.target.value
|
||||
};
|
||||
var upProperties = this.state.data.slice();
|
||||
var newitem = upProperties.map((item) => {
|
||||
for (var key in item) {
|
||||
if (key == newProp.name && item.UserID == newProp.id) {
|
||||
item[key] = newProp.value;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setState({ data: newitem });
|
||||
}
|
||||
|
||||
private updateWithManualProperty = () => {
|
||||
this.props.UpdateSPUserWithManualProps(this.state.data);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { data } = this.state;
|
||||
const { showProgress } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{(data && data.length > 0) ? (
|
||||
<>
|
||||
<EditableTable onTableUpdate={this.handlePropertyTable.bind(this)} onRowDel={this.handleRowDel.bind(this)}
|
||||
data={data} />
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<PrimaryButton text={strings.BtnUpdateUserProps} onClick={this.updateWithManualProperty} style={{ marginRight: '5px' }} disabled={showProgress} />
|
||||
{showProgress && <Spinner className={styles.generateTemplateLoader} label={strings.PropsUpdateLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div><MessageContainer MessageScope={MessageScope.Info} Message={strings.EmptyTable} /></div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import styles from './SpupsProperySync.module.scss';
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
import { Text } from 'office-ui-fabric-react/lib/Text';
|
||||
import { MessageScope } from '../../../Common/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,61 @@
|
|||
@import '../../../../Common/theming';
|
||||
|
||||
.propertyMappingPanelContent {
|
||||
.mappingcontainer {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.propertytitlediv {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
padding: 0px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.togglediv {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.propertydiv {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,.2), 0 0px 0px 0 rgba(0,0,0,.1);
|
||||
padding: 10px 3px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
border: 1px solid;
|
||||
border-color: $ms-themeTertiary;
|
||||
}
|
||||
.separator {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
i {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
.separator::before {
|
||||
background-color: $ms-themeLighter;
|
||||
}
|
||||
}
|
||||
.panelHeader {
|
||||
margin-top: -29px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px dotted;
|
||||
font-weight: 700;
|
||||
}
|
||||
.panelFooter {
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.generateTemplateLoader {
|
||||
display: inline-flex;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.propertyMappingList {
|
||||
padding: 0px;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import * as React from 'react';
|
||||
import { Separator } from 'office-ui-fabric-react/lib/Separator';
|
||||
import { Icon, IIconStyles } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||
import styles from './PropertyMapping.module.scss';
|
||||
import { IPropertyMappings } from '../../../../Common/IModel';
|
||||
|
||||
const iconStyles: IIconStyles = {
|
||||
root: {
|
||||
fontSize: '24px',
|
||||
height: '24px',
|
||||
width: '24px'
|
||||
}
|
||||
};
|
||||
|
||||
export interface IPropertyMappingItemProps {
|
||||
items: IPropertyMappings[];
|
||||
onEnableOrDisableProperty: (item: IPropertyMappings, checked: boolean) => void;
|
||||
}
|
||||
|
||||
export default class PropertyMappingItem extends React.Component<IPropertyMappingItemProps, {}> {
|
||||
constructor(props: IPropertyMappingItemProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { items } = this.props;
|
||||
return (
|
||||
<>
|
||||
{items.map((item: IPropertyMappings) => {
|
||||
return (
|
||||
<div className={styles.mappingcontainer} data-is-focusable={true}>
|
||||
<div className={styles.propertydiv}>{item.AzProperty}</div>
|
||||
<Separator className={styles.separator}>
|
||||
<Icon iconName="DoubleChevronRight8" styles={iconStyles} />
|
||||
</Separator>
|
||||
<div className={styles.propertydiv}>{item.SPProperty}</div>
|
||||
<div className={styles.togglediv}>
|
||||
<Toggle label="" checked={item.IsIncluded} onChange={(e, checked) => { this.props.onEnableOrDisableProperty(item, checked); }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
import * as React from 'react';
|
||||
import styles from './PropertyMapping.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
import { IIconProps } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import SPHelper from '../../../../Common/SPHelper';
|
||||
import { IPropertyMappings, MessageScope } from '../../../../Common/IModel';
|
||||
import PropertyMappingItem from './PropertyMappingItem';
|
||||
import { parse } from 'json2csv';
|
||||
import MessageContainer from '../MessageContainer';
|
||||
|
||||
const filter: any = require('lodash/filter');
|
||||
|
||||
const downloadIcon: IIconProps = { iconName: 'SaveTemplate' };
|
||||
const csvIcon: IIconProps = { iconName: 'FileTemplate' };
|
||||
|
||||
export interface IPropertyMappingProps {
|
||||
mappingProperties: IPropertyMappings[];
|
||||
helper: SPHelper;
|
||||
disabled: boolean;
|
||||
siteurl: string;
|
||||
}
|
||||
|
||||
export interface IPropertyMappingState {
|
||||
isOpen: boolean;
|
||||
templateProperties: IPropertyMappings[];
|
||||
downloadLink: string;
|
||||
templateFileName: string;
|
||||
showProgress: boolean;
|
||||
disableButtons: boolean;
|
||||
disableMappingButton: boolean;
|
||||
globalMessage: string;
|
||||
globalMessageScope?: MessageScope;
|
||||
}
|
||||
|
||||
export default class PropertyMappingList extends React.Component<IPropertyMappingProps, IPropertyMappingState> {
|
||||
private includedProperties: IPropertyMappings[] = [];
|
||||
/**
|
||||
* Default constructor
|
||||
* @param props
|
||||
*/
|
||||
constructor(props: IPropertyMappingProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
templateProperties: [],
|
||||
downloadLink: '',
|
||||
templateFileName: '',
|
||||
showProgress: false,
|
||||
disableButtons: false,
|
||||
disableMappingButton: false,
|
||||
globalMessage: ""
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Component mount
|
||||
*/
|
||||
public componentDidMount = () => {
|
||||
let templateProperties: IPropertyMappings[] = this.getDefaultTemplateProperties();
|
||||
this.setState({ templateProperties });
|
||||
}
|
||||
/**
|
||||
* Component updated
|
||||
*/
|
||||
public componentDidUpdate = (prevProps: IPropertyMappingProps) => {
|
||||
if (prevProps.mappingProperties !== this.props.mappingProperties ||
|
||||
prevProps.disabled !== this.props.disabled) {
|
||||
let templateProperties: IPropertyMappings[] = this.getDefaultTemplateProperties();
|
||||
this.setState({
|
||||
templateProperties,
|
||||
disableMappingButton: this.props.disabled
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the property mappings from the props
|
||||
*/
|
||||
private getDefaultTemplateProperties = () => {
|
||||
let defaultProps: IPropertyMappings[] = this.props.mappingProperties;
|
||||
let globalMessage: string = "";
|
||||
if (defaultProps.length <= 0) globalMessage = strings.EmptyPropertyMappings;
|
||||
else globalMessage = "";
|
||||
this.setState({ globalMessage, globalMessageScope: MessageScope.Failure, disableButtons: globalMessage.length > 0, disableMappingButton: globalMessage.length > 0 });
|
||||
return defaultProps;
|
||||
}
|
||||
/**
|
||||
* Update the property mappings state by enabling or disabling the property
|
||||
* Based on this the templates will be generated
|
||||
*/
|
||||
private _onEnableOrDisableProperty = (item: IPropertyMappings, checked: boolean) => {
|
||||
let templateProperties: IPropertyMappings[] = this.state.templateProperties;
|
||||
let property = templateProperties.filter(prop => { return prop.ID == item.ID; });
|
||||
if (property) property[0].IsIncluded = false;
|
||||
this.setState({ ...this.state, templateProperties });
|
||||
this.render();
|
||||
}
|
||||
/**
|
||||
* Get the default property mappings and then open the panel
|
||||
*/
|
||||
private _openPropertyMappingPanel = () => {
|
||||
let templateProperties = this.getDefaultTemplateProperties();
|
||||
this.setState({ templateProperties, isOpen: true });
|
||||
}
|
||||
/**
|
||||
* Dismiss or close the panel
|
||||
*/
|
||||
private _dismissPanel = () => {
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
/**
|
||||
* Custom panel footer contents with buttons
|
||||
*/
|
||||
private _onRenderPanelFooterContent = (): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.panelFooter}>
|
||||
<PrimaryButton iconProps={downloadIcon} onClick={this._generateJSONTemplate} disabled={this.state.disableButtons}>{strings.BtnGenerateJSON}</PrimaryButton>
|
||||
<PrimaryButton iconProps={csvIcon} onClick={this._generateCSVTemplate} disabled={this.state.disableButtons}>{strings.BtnGenerateCSV}</PrimaryButton>
|
||||
{this.state.showProgress && <Spinner className={styles.generateTemplateLoader} label={strings.GenerateTemplateLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Get the property mappings that are included by the user
|
||||
*/
|
||||
private _getIncludedPropertyMapping = () => {
|
||||
return filter(this.state.templateProperties, (o) => { return o.IsIncluded; });
|
||||
}
|
||||
/**
|
||||
* Button click to generate the JSON template
|
||||
*/
|
||||
private _generateJSONTemplate = async () => {
|
||||
this.setState({ disableButtons: true, showProgress: true });
|
||||
const { helper } = this.props;
|
||||
let jsonOut = await helper.getPropertyMappingsTemplate(this._getIncludedPropertyMapping());
|
||||
let fileTemplate = await helper.addFilesToFolder(JSON.stringify(jsonOut), false);
|
||||
this.setState({
|
||||
downloadLink: fileTemplate.data.ServerRelativeUrl,
|
||||
templateFileName: fileTemplate.data.Name
|
||||
}, this.getTemplateFile);
|
||||
}
|
||||
/**
|
||||
* Button click to generate the CSV template
|
||||
*/
|
||||
private _generateCSVTemplate = async () => {
|
||||
this.setState({ disableButtons: true, showProgress: true });
|
||||
const { helper } = this.props;
|
||||
let templateProperties = this._getIncludedPropertyMapping();
|
||||
let fields: string[] = [];
|
||||
fields.push("UserID");
|
||||
templateProperties.map(propmap => {
|
||||
fields.push(propmap.SPProperty);
|
||||
});
|
||||
const csv = parse("", { fields });
|
||||
let fileTemplate = await helper.addFilesToFolder(csv, true);
|
||||
this.setState({
|
||||
downloadLink: fileTemplate.data.ServerRelativeUrl,
|
||||
templateFileName: fileTemplate.data.Name
|
||||
}, this.getTemplateFile);
|
||||
}
|
||||
/**
|
||||
* Download the JSON file
|
||||
*/
|
||||
private getTemplateFile = async () => {
|
||||
const anchor = window.document.createElement('a');
|
||||
anchor.href = `${this.props.siteurl}/_layouts/15/download.aspx?SourceUrl=${this.state.downloadLink}`;
|
||||
anchor.download = this.state.templateFileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
this.setState({ disableButtons: false, showProgress: false, globalMessage: strings.TemplateDownloaded, globalMessageScope: MessageScope.Success });
|
||||
}
|
||||
/**
|
||||
* Component render
|
||||
*/
|
||||
public render(): JSX.Element {
|
||||
const { isOpen, templateProperties, disableMappingButton, globalMessage, globalMessageScope } = this.state;
|
||||
return (
|
||||
<div className={styles.propertyMappingList}>
|
||||
<PrimaryButton text={strings.BtnPropertyMapping} onClick={this._openPropertyMappingPanel} disabled={disableMappingButton} />
|
||||
<Panel isOpen={isOpen} onDismiss={this._dismissPanel} type={PanelType.largeFixed} closeButtonAriaLabel="Close" headerText={strings.PnlHeaderText}
|
||||
headerClassName={styles.panelHeader} isFooterAtBottom={true} onRenderFooterContent={this._onRenderPanelFooterContent}>
|
||||
<div className={styles.propertyMappingPanelContent}>
|
||||
{globalMessage.length > 0 &&
|
||||
<div style={{ marginTop: '10px', marginBottom: '10px' }}>
|
||||
<MessageContainer MessageScope={globalMessageScope} Message={globalMessage} />
|
||||
</div>
|
||||
}
|
||||
<div className={styles.mappingcontainer} data-is-focusable={true} style={{ marginBottom: '10px' }}>
|
||||
<div className={styles.propertytitlediv}>{strings.TblColHeadAzProperty}</div>
|
||||
<div className={styles.separator}> </div>
|
||||
<div className={styles.propertytitlediv}>{strings.TblColHeadSPProperty}</div>
|
||||
<div className={styles.propertytitlediv} style={{ padding: '0px' }}>{strings.TblColHeadEnableDisable}</div>
|
||||
</div>
|
||||
<PropertyMappingItem items={templateProperties} onEnableOrDisableProperty={this._onEnableOrDisableProperty} />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
//@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
@import '../../../Common/theming';
|
||||
|
||||
.spupsProperySync {
|
||||
.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);
|
||||
}
|
||||
|
||||
div.ms-BasePicker-text {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.menuContent {
|
||||
padding: 8px;
|
||||
}
|
||||
.emptyData {
|
||||
color: $ms-error;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.generateTemplateLoader {
|
||||
display: inline-flex;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.status {font-weight: bold;}
|
||||
.status.blue { i { color: blue; } }
|
||||
.status.green { i { color: green; } }
|
||||
.status.orange { i { color: orange; } }
|
||||
.status.red { i { color: red; } }
|
||||
.resultsIconSpan {
|
||||
top: 7px;
|
||||
position: absolute;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.searchcontainer {
|
||||
width: 95%;
|
||||
display: inline-block;
|
||||
}
|
||||
.refreshContainer {
|
||||
display: inline-flex;
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.syncjobsContainer {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
button {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.templatesContainer {
|
||||
max-height: 500px;
|
||||
button {
|
||||
height: 24px;
|
||||
}
|
||||
div.templateList {
|
||||
overflow-y: auto;
|
||||
height: 450px;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.uppropertydata {
|
||||
.uppropertylist {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
.downloadLoaderDiv {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
position: absolute;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.fileiconDiv {
|
||||
width: 5%;
|
||||
display: inline-block;
|
||||
i {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
}
|
||||
}
|
||||
.propertyMappingContainer {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
.propertydiv {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,.2), 0 0px 0px 0 rgba(0,0,0,.1);
|
||||
padding: 10px 3px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
border: 1px solid;
|
||||
border-color: $ms-themeTertiary;
|
||||
}
|
||||
}
|
||||
.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,588 @@
|
|||
import * as React from 'react';
|
||||
import styles from './SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { PeoplePicker, PrincipalType } from "@pnp/spfx-controls-react/lib/PeoplePicker";
|
||||
import { IPropertyFieldGroupOrPerson } from '@pnp/spfx-property-controls/lib/propertyFields/peoplePicker/IPropertyFieldPeoplePicker';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { FilePicker, IFilePickerResult } from '@pnp/spfx-controls-react/lib/FilePicker';
|
||||
import { FileTypeIcon, IconType } from "@pnp/spfx-controls-react/lib/FileTypeIcon";
|
||||
import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { css, ProgressIndicator } from 'office-ui-fabric-react/lib';
|
||||
import { IPropertyMappings, FileContentType, MessageScope, SyncType } from '../../../Common/IModel';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import SPHelper from '../../../Common/SPHelper';
|
||||
import PropertyMappingList from './PropertyMapping/PropertyMappingList';
|
||||
import UPPropertyData from './UPPropertyData';
|
||||
import ManualPropertyUpdate from './ManualPropertyUpdate';
|
||||
import AzurePropertyView from './AzurePropertyView';
|
||||
import SyncJobsView from './SyncJobs/SyncJobs';
|
||||
import TemplatesView from './TemplatesList/TemplatesView';
|
||||
import BulkSyncList from './BulkSyncFiles/BulkSyncList';
|
||||
import * as moment from 'moment/moment';
|
||||
import MessageContainer from './MessageContainer';
|
||||
|
||||
|
||||
const map: any = require('lodash/map');
|
||||
|
||||
export interface ISpupsProperySyncProps {
|
||||
context: WebPartContext;
|
||||
templateLib: string;
|
||||
displayMode: DisplayMode;
|
||||
appTitle: string;
|
||||
AzFuncUrl: string;
|
||||
UseCert: boolean;
|
||||
dateFormat: string;
|
||||
allowedUsers: IPropertyFieldGroupOrPerson[];
|
||||
useFullWidth: boolean;
|
||||
openPropertyPane: () => void;
|
||||
updateProperty: (value: string) => void;
|
||||
}
|
||||
|
||||
export interface ISpupsProperySyncState {
|
||||
listExists: boolean;
|
||||
isSiteAdmin: boolean;
|
||||
loading: boolean;
|
||||
accessDenied: boolean;
|
||||
propertyMappings: IPropertyMappings[];
|
||||
uploadedTemplate?: IFilePickerResult;
|
||||
uploadedFileURL?: string;
|
||||
showUploadData: boolean;
|
||||
showUploadProgress: boolean;
|
||||
showPropsLoader: boolean;
|
||||
updatePropsLoader_Manual: boolean;
|
||||
updatePropsLoader_Azure: boolean;
|
||||
updatePropsLoader_Bulk: boolean;
|
||||
clearData: boolean;
|
||||
disablePropsButtons: boolean;
|
||||
uploadedData?: any;
|
||||
isCSV: boolean;
|
||||
selectedUsers?: any[];
|
||||
manualPropertyData: any[];
|
||||
azurePropertyData: any[];
|
||||
reloadGetProperties: boolean;
|
||||
helper: SPHelper;
|
||||
selectedMenu?: string;
|
||||
globalMessage: string;
|
||||
noActivePropertyMappings: boolean;
|
||||
}
|
||||
|
||||
export default class SpupsProperySync extends React.Component<ISpupsProperySyncProps, ISpupsProperySyncState> {
|
||||
// Private variables
|
||||
private helper: SPHelper = null;
|
||||
/**
|
||||
* Constructor
|
||||
* @param props
|
||||
*/
|
||||
constructor(props: ISpupsProperySyncProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
listExists: false,
|
||||
isSiteAdmin: false,
|
||||
loading: true,
|
||||
accessDenied: false,
|
||||
propertyMappings: [],
|
||||
showUploadData: false,
|
||||
showUploadProgress: false,
|
||||
showPropsLoader: false,
|
||||
updatePropsLoader_Manual: false,
|
||||
updatePropsLoader_Azure: false,
|
||||
updatePropsLoader_Bulk: false,
|
||||
clearData: false,
|
||||
disablePropsButtons: false,
|
||||
isCSV: false,
|
||||
selectedUsers: [],
|
||||
manualPropertyData: [],
|
||||
azurePropertyData: [],
|
||||
reloadGetProperties: false,
|
||||
helper: null,
|
||||
selectedMenu: '0',
|
||||
globalMessage: '',
|
||||
noActivePropertyMappings: true
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Component mount
|
||||
*/
|
||||
public componentDidMount = async () => {
|
||||
this._useFullWidth();
|
||||
this.initializeHelper();
|
||||
let currentUserInfo = await this.helper.getCurrentUserInfo();
|
||||
if (currentUserInfo.IsSiteAdmin) {
|
||||
this.setState({ isSiteAdmin: true });
|
||||
this._checkAndCreateLists();
|
||||
} else {
|
||||
let allowedGroups: string[] = map(this.props.allowedUsers, 'login');
|
||||
let accessAllowed: boolean = this.helper.checkCurrentUserGroup(allowedGroups, currentUserInfo.Groups);
|
||||
console.log(accessAllowed);
|
||||
if (accessAllowed) {
|
||||
this._checkAndCreateLists();
|
||||
} else {
|
||||
this.setState({ loading: false, accessDenied: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Component update
|
||||
*/
|
||||
public componentDidUpdate = (prevProps: ISpupsProperySyncProps) => {
|
||||
if (prevProps.templateLib !== this.props.templateLib) this.initializeHelper();
|
||||
//if (prevProps.appTitle !== this.props.appTitle || prevProps.dateFormat !== this.props.dateFormat || this.props.allowedUsers) this.render();
|
||||
if (prevProps.useFullWidth !== this.props.useFullWidth) this._useFullWidth();
|
||||
}
|
||||
/**
|
||||
* Check and create the required list
|
||||
*/
|
||||
public _checkAndCreateLists = async () => {
|
||||
this.setState({ loading: false });
|
||||
let listExists = await this.helper.checkAndCreateLists();
|
||||
if (listExists) {
|
||||
let propertyMappings: IPropertyMappings[] = await this.helper.getPropertyMappings();
|
||||
let globalMessage: string = "";
|
||||
let noActivePropertyMappings: boolean = true;
|
||||
if (propertyMappings.length <= 0) {
|
||||
globalMessage = strings.EmptyPropertyMappings;
|
||||
noActivePropertyMappings = true;
|
||||
} else {
|
||||
globalMessage = "";
|
||||
noActivePropertyMappings = false;
|
||||
}
|
||||
propertyMappings.map(prop => { prop.IsIncluded = true; });
|
||||
this.setState({ listExists, propertyMappings, globalMessage, noActivePropertyMappings, disablePropsButtons: noActivePropertyMappings });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Initialize the helper with required arguments.
|
||||
*/
|
||||
private initializeHelper = () => {
|
||||
this.helper = new SPHelper(this.props.context.pageContext.legacyPageContext.siteAbsoluteUrl,
|
||||
this.props.context.pageContext.legacyPageContext.tenantDisplayName,
|
||||
this.props.context.pageContext.legacyPageContext.webDomain,
|
||||
this.props.context.pageContext.web.serverRelativeUrl,
|
||||
this.props.templateLib
|
||||
);
|
||||
this.setState({ helper: this.helper });
|
||||
}
|
||||
/**
|
||||
* Use full width
|
||||
*/
|
||||
private _useFullWidth = () => {
|
||||
if (this.props.useFullWidth) {
|
||||
const jQuery: any = require('jquery');
|
||||
jQuery("#workbenchPageContent").prop("style", "max-width: none");
|
||||
jQuery(".SPCanvas-canvas").prop("style", "max-width: none");
|
||||
jQuery(".CanvasZone").prop("style", "max-width: none");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Triggers when the users are selected for manual update
|
||||
*/
|
||||
private _getPeoplePickerItems = (items: any[]) => {
|
||||
let reloadGetProperties: boolean = false;
|
||||
if (this.state.selectedUsers.length > items.length) {
|
||||
if (this.state.manualPropertyData.length > 0 || this.state.azurePropertyData.length > 0) {
|
||||
reloadGetProperties = true;
|
||||
}
|
||||
}
|
||||
this.setState({ selectedUsers: items, reloadGetProperties, clearData: false }, () => {
|
||||
if (this.state.selectedUsers.length <= 0) {
|
||||
this.state.manualPropertyData.length > 0 ? this._getManualPropertyTable() : this._getAzurePropertyTable();
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Set the defaultusers property for people picker control, this is used when clearing the data.
|
||||
*/
|
||||
private _getSelectedUsersLoginNames = (items: any[]): string[] => {
|
||||
let retUsers: string[] = [];
|
||||
retUsers = map(items, (o) => { return o.loginName.split('|')[2]; });
|
||||
return retUsers;
|
||||
}
|
||||
/**
|
||||
* Display the inline editing table to edit the properties for manual update
|
||||
*/
|
||||
private _getManualPropertyTable = () => {
|
||||
this.setState({ disablePropsButtons: true, showPropsLoader: true });
|
||||
const { propertyMappings, selectedUsers } = this.state;
|
||||
let includedProperties: IPropertyMappings[] = propertyMappings.filter((o) => { return o.IsIncluded; });
|
||||
let manualPropertyData: any[] = [];
|
||||
if (selectedUsers && selectedUsers.length > 0) {
|
||||
selectedUsers.map(user => {
|
||||
let userObj = new Object();
|
||||
userObj['UserID'] = user.loginName;
|
||||
userObj['DisplayName'] = user.text;
|
||||
userObj['ImageUrl'] = user.imageUrl;
|
||||
includedProperties.map((propsMap: IPropertyMappings) => {
|
||||
userObj[propsMap.SPProperty] = "";
|
||||
});
|
||||
manualPropertyData.push(userObj);
|
||||
});
|
||||
this.setState({ manualPropertyData, azurePropertyData: [], showPropsLoader: false, disablePropsButtons: false });
|
||||
} else {
|
||||
this.setState({ disablePropsButtons: false, showPropsLoader: false, manualPropertyData: [] });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the property values from Azure
|
||||
*/
|
||||
private _getAzurePropertyTable = async () => {
|
||||
this.setState({ disablePropsButtons: true, showPropsLoader: true });
|
||||
const { propertyMappings, selectedUsers } = this.state;
|
||||
let includedProperties: IPropertyMappings[] = propertyMappings.filter((o) => { return o.IsIncluded; });
|
||||
let selectFields: string = "id, userPrincipalName, displayName, " + map(includedProperties, 'AzProperty').join(',');
|
||||
let tempQuery: string[] = []; let filterQuery: string = ``;
|
||||
if (selectedUsers && selectedUsers.length > 0) {
|
||||
selectedUsers.map(user => {
|
||||
tempQuery.push(`userPrincipalName eq '${user.loginName.split('|')[2]}'`);
|
||||
});
|
||||
filterQuery = tempQuery.join(' or ');
|
||||
let azurePropertyData = await this.helper.getAzurePropertyForUsers(selectFields, filterQuery);
|
||||
this.setState({ azurePropertyData, manualPropertyData: [], showPropsLoader: false, disablePropsButtons: false });
|
||||
} else {
|
||||
this.setState({ disablePropsButtons: false, showPropsLoader: false, azurePropertyData: [] });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* On selecting the data file for update
|
||||
*/
|
||||
private _onSaveTemplate = (uploadedTemplate: IFilePickerResult) => {
|
||||
this.setState({ uploadedTemplate, showUploadData: true, clearData: false });
|
||||
}
|
||||
/**
|
||||
* On changing the data file for update
|
||||
*/
|
||||
private _onChangeTemplate = (uploadedTemplate: IFilePickerResult) => {
|
||||
this.setState({ uploadedTemplate, showUploadData: true, clearData: false });
|
||||
}
|
||||
/**
|
||||
* Uploading data file and displaying the contents of the file
|
||||
*/
|
||||
private _uploadDataToSync = async () => {
|
||||
this.setState({ showUploadProgress: true });
|
||||
const { uploadedTemplate } = this.state;
|
||||
let filecontent: any = null;
|
||||
if (uploadedTemplate && uploadedTemplate.fileName) {
|
||||
let ext: string = uploadedTemplate.fileName.split('.').pop();
|
||||
let filename: string = `${uploadedTemplate.fileNameWithoutExtension}_${moment().format("MMDDYYYYHHmmss")}.${ext}`;
|
||||
if (uploadedTemplate.fileAbsoluteUrl && null !== uploadedTemplate.fileAbsoluteUrl) {
|
||||
let filerelativeurl: string = "";
|
||||
if (uploadedTemplate.fileAbsoluteUrl.indexOf(this.props.context.pageContext.legacyPageContext.webAbsoluteUrl) >= 0) {
|
||||
filerelativeurl = uploadedTemplate.fileAbsoluteUrl.replace(this.props.context.pageContext.legacyPageContext.webAbsoluteUrl,
|
||||
this.props.context.pageContext.legacyPageContext.webServerRelativeUrl);
|
||||
}
|
||||
filecontent = await this.helper.getFileContent(filerelativeurl, FileContentType.Blob);
|
||||
await this.helper.addDataFilesToFolder(filecontent, filename);
|
||||
if (ext.toLocaleLowerCase() == "csv") {
|
||||
filecontent = await this.helper.getFileContent(filerelativeurl, FileContentType.Text);
|
||||
} else if (ext.toLocaleLowerCase() == "json") {
|
||||
filecontent = await this.helper.getFileContent(filerelativeurl, FileContentType.JSON);
|
||||
}
|
||||
this.setState({ showUploadProgress: false, uploadedData: filecontent, isCSV: ext.toLocaleLowerCase() == "csv" });
|
||||
} else {
|
||||
let dataToSync = await uploadedTemplate.downloadFileContent();
|
||||
let filereader = new FileReader();
|
||||
filereader.readAsBinaryString(dataToSync);
|
||||
filereader.onload = async () => {
|
||||
let dataUploaded = await this.helper.addDataFilesToFolder(filereader.result, filename);
|
||||
if (ext.toLocaleLowerCase() == "csv") {
|
||||
filecontent = await dataUploaded.file.getText();
|
||||
} else if (ext.toLocaleLowerCase() == "json") {
|
||||
filecontent = await dataUploaded.file.getJSON();
|
||||
}
|
||||
this.setState({ showUploadProgress: false, uploadedData: filecontent, isCSV: ext.toLocaleLowerCase() == "csv" });
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update with manual properties
|
||||
*/
|
||||
private _updateSPWithManualProperties = async (data: any[]) => {
|
||||
this.setState({ updatePropsLoader_Manual: true });
|
||||
let itemID = await this.helper.createSyncItem(SyncType.Manual);
|
||||
let finalJson = this._prepareJSONForAzFunc(data, false, itemID);
|
||||
await this.helper.updateSyncItem(itemID, finalJson);
|
||||
this.helper.runAzFunction(this.props.context.httpClient, finalJson, this.props.AzFuncUrl, itemID);
|
||||
this.setState({ updatePropsLoader_Manual: false, clearData: true, selectedUsers: [], manualPropertyData: [] });
|
||||
}
|
||||
/**
|
||||
* Update with azure properties
|
||||
*/
|
||||
private _updateSPWithAzureProperties = async (data: any[]) => {
|
||||
this.setState({ updatePropsLoader_Azure: true });
|
||||
let itemID = await this.helper.createSyncItem(SyncType.Azure);
|
||||
let finalJson = this._prepareJSONForAzFunc(data, true, itemID);
|
||||
await this.helper.updateSyncItem(itemID, finalJson);
|
||||
this.helper.runAzFunction(this.props.context.httpClient, finalJson, this.props.AzFuncUrl, itemID);
|
||||
this.setState({ updatePropsLoader_Azure: false, clearData: true, selectedUsers: [], azurePropertyData: [] });
|
||||
}
|
||||
/**
|
||||
* Update with csv or json file
|
||||
*/
|
||||
private _updateSPForBulkUsers = async (data: any[]) => {
|
||||
this.setState({ updatePropsLoader_Bulk: true });
|
||||
let itemID = await this.helper.createSyncItem(SyncType.Template);
|
||||
let finalJson = this._prepareJSONForAzFunc(data, false, itemID);
|
||||
await this.helper.updateSyncItem(itemID, finalJson);
|
||||
this.helper.runAzFunction(this.props.context.httpClient, finalJson, this.props.AzFuncUrl, itemID);
|
||||
this.setState({ updatePropsLoader_Bulk: false, clearData: true, uploadedData: null, uploadedTemplate: null, uploadedFileURL: '', showUploadData: false });
|
||||
}
|
||||
/**
|
||||
* Prepare JSON based on the manual or az data to call AZ FUNC.
|
||||
*/
|
||||
private _prepareJSONForAzFunc = (data: any[], isAzure: boolean, itemid: number): string => {
|
||||
let finalJson: string = "";
|
||||
if (data && data.length > 0) {
|
||||
let userPropMapping = new Object();
|
||||
userPropMapping['targetSiteUrl'] = this.props.context.pageContext.legacyPageContext.webAbsoluteUrl;
|
||||
userPropMapping['targetAdminUrl'] = `https://${this.props.context.pageContext.legacyPageContext.tenantDisplayName}-admin.${this.props.context.pageContext.legacyPageContext.webDomain}`;
|
||||
userPropMapping['usecert'] = this.props.UseCert ? this.props.UseCert : false;
|
||||
userPropMapping['itemId'] = itemid;
|
||||
let propValues: any[] = [];
|
||||
data.map((userprop: any) => {
|
||||
let userPropValue: any = {};
|
||||
let userProperties: any[] = [];
|
||||
let userPropertiesKeys: string[] = Object.keys(userprop);
|
||||
userPropertiesKeys.map((prop: string) => {
|
||||
if (isAzure && prop.toLowerCase() == "userprincipalname") {
|
||||
userPropValue['userid'] = userprop[prop].indexOf('|') > 0 ? userprop[prop].split('|')[2] : userprop[prop];
|
||||
}
|
||||
if (!isAzure && prop.toLowerCase() == "userid") {
|
||||
userPropValue['userid'] = userprop[prop].indexOf('|') > 0 ? userprop[prop].split('|')[2] : userprop[prop];
|
||||
}
|
||||
if (prop.toLowerCase() !== "userid" && prop.toLowerCase() !== "id" && prop.toLowerCase() !== "displayname"
|
||||
&& prop.toLowerCase() !== "userprincipalname" && prop.toLowerCase() !== "imageurl") {
|
||||
let objProp = new Object();
|
||||
objProp['name'] = isAzure ? this._getSPPropertyName(prop) : prop;
|
||||
objProp['value'] = userprop[prop];
|
||||
userProperties.push(JSON.parse(JSON.stringify(objProp)));
|
||||
}
|
||||
});
|
||||
userPropValue['properties'] = JSON.parse(JSON.stringify(userProperties));
|
||||
propValues.push(JSON.parse(JSON.stringify(userPropValue)));
|
||||
});
|
||||
userPropMapping['value'] = propValues;
|
||||
finalJson = JSON.stringify(userPropMapping);
|
||||
}
|
||||
return finalJson;
|
||||
}
|
||||
/**
|
||||
* Get SPProperty name for Azure Property
|
||||
*/
|
||||
private _getSPPropertyName = (azPropName: string): string => {
|
||||
return this.state.propertyMappings.filter((o) => { return o.AzProperty.toLowerCase() === azPropName.toLowerCase(); })[0].SPProperty;
|
||||
}
|
||||
/**
|
||||
* On menu click
|
||||
*/
|
||||
private _onMenuClick = (item?: PivotItem, ev?: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||
if (item) {
|
||||
if (item.props.itemKey == "0") {
|
||||
this.setState({
|
||||
updatePropsLoader_Manual: false, updatePropsLoader_Azure: false, clearData: false, selectedUsers: [],
|
||||
manualPropertyData: [], azurePropertyData: []
|
||||
});
|
||||
} else if (item.props.itemKey == "1") {
|
||||
this.setState({ uploadedData: null, uploadedTemplate: null, uploadedFileURL: '', showUploadData: false });
|
||||
}
|
||||
this.setState({
|
||||
selectedMenu: item.props.itemKey
|
||||
}, () => {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Component render
|
||||
*/
|
||||
public render(): React.ReactElement<ISpupsProperySyncProps> {
|
||||
const { templateLib, displayMode, appTitle, AzFuncUrl } = this.props;
|
||||
const { propertyMappings, uploadedTemplate, uploadedFileURL, showUploadData, showUploadProgress, uploadedData, isCSV, selectedUsers, manualPropertyData,
|
||||
azurePropertyData, disablePropsButtons, showPropsLoader, reloadGetProperties, selectedMenu, updatePropsLoader_Manual, updatePropsLoader_Azure,
|
||||
updatePropsLoader_Bulk, clearData, globalMessage, noActivePropertyMappings, listExists, isSiteAdmin, loading, accessDenied } = this.state;
|
||||
const fileurl = uploadedFileURL ? uploadedFileURL : uploadedTemplate && uploadedTemplate.fileAbsoluteUrl ? uploadedTemplate.fileAbsoluteUrl :
|
||||
uploadedTemplate && uploadedTemplate.fileName ? uploadedTemplate.fileName : '';
|
||||
const showConfig = !templateLib || !AzFuncUrl ? true : false;
|
||||
const headerButtonProps = { 'disabled': showUploadProgress || updatePropsLoader_Manual || updatePropsLoader_Azure || updatePropsLoader_Bulk };
|
||||
return (
|
||||
<div className={styles.spupsProperySync}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.column}>
|
||||
<WebPartTitle displayMode={displayMode} title={appTitle ? appTitle : strings.DefaultAppTitle} updateProperty={this.props.updateProperty} />
|
||||
{showConfig ? (
|
||||
<>
|
||||
{isSiteAdmin ? (
|
||||
<Placeholder iconName='DataManagementSettings'
|
||||
iconText={strings.PlaceholderIconText}
|
||||
description={strings.PlaceholderDescription}
|
||||
buttonLabel={strings.PlaceholderButtonLabel}
|
||||
hideButton={displayMode === DisplayMode.Read}
|
||||
onConfigure={this.props.openPropertyPane} />
|
||||
) : (
|
||||
<>
|
||||
{loading &&
|
||||
<ProgressIndicator label={strings.SitePrivilegeCheckLabel} description={strings.PropsLoader} />
|
||||
}
|
||||
{!loading &&
|
||||
<MessageContainer MessageScope={MessageScope.SevereWarning} Message={strings.AdminConfigHelp} />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{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>
|
||||
{globalMessage.length > 0 &&
|
||||
<div style={{ marginTop: '10px', marginBottom: '10px' }}>
|
||||
<MessageContainer MessageScope={MessageScope.Failure} Message={globalMessage} />
|
||||
</div>
|
||||
}
|
||||
<Pivot defaultSelectedKey="0" selectedKey={selectedMenu} onLinkClick={this._onMenuClick} className={styles.periodmenu}>
|
||||
<PivotItem headerText={strings.TabMenu1} itemKey="0" itemIcon="SchoolDataSyncLogo" headerButtonProps={headerButtonProps} ></PivotItem>
|
||||
<PivotItem headerText={strings.TabMenu2} itemKey="1" itemIcon="BulkUpload" headerButtonProps={headerButtonProps}></PivotItem>
|
||||
<PivotItem headerText={strings.TabMenu3} itemKey="2" itemIcon="StackIndicator" headerButtonProps={headerButtonProps}></PivotItem>
|
||||
<PivotItem headerText={strings.TabMenu4} itemKey="3" itemIcon="FileTemplate" headerButtonProps={headerButtonProps}></PivotItem>
|
||||
<PivotItem headerText={strings.TabMenu5} itemKey="4" itemIcon="SyncStatus" headerButtonProps={headerButtonProps}></PivotItem>
|
||||
</Pivot>
|
||||
<div style={{ float: "right" }}>
|
||||
<PropertyMappingList mappingProperties={propertyMappings} helper={this.state.helper} siteurl={this.props.context.pageContext.web.serverRelativeUrl}
|
||||
disabled={showUploadProgress || updatePropsLoader_Manual || updatePropsLoader_Azure || updatePropsLoader_Bulk || noActivePropertyMappings} />
|
||||
</div>
|
||||
</div>
|
||||
{selectedMenu == "0" &&
|
||||
<div className={css(styles.menuContent)}>
|
||||
<PeoplePicker
|
||||
disabled={disablePropsButtons || updatePropsLoader_Manual || updatePropsLoader_Azure}
|
||||
context={this.props.context}
|
||||
titleText={strings.PPLPickerTitleText}
|
||||
personSelectionLimit={10}
|
||||
groupName={""} // Leave this blank in case you want to filter from all users
|
||||
showtooltip={false}
|
||||
isRequired={false}
|
||||
selectedItems={this._getPeoplePickerItems}
|
||||
showHiddenInUI={false}
|
||||
principalTypes={[PrincipalType.User]}
|
||||
resolveDelay={500}
|
||||
defaultSelectedUsers={selectedUsers.length > 0 ? this._getSelectedUsersLoginNames(selectedUsers) : []} />
|
||||
{reloadGetProperties ? (
|
||||
<>
|
||||
{selectedUsers.length > 0 &&
|
||||
<div>
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.UserListChanges} />
|
||||
</div>
|
||||
}
|
||||
{selectedUsers.length <= 0 && !clearData &&
|
||||
<div>
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.UserListEmpty} ShowDismiss={true} />
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
{selectedUsers && selectedUsers.length > 0 &&
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<PrimaryButton text={strings.BtnManualProps} onClick={this._getManualPropertyTable} style={{ marginRight: '5px' }}
|
||||
disabled={disablePropsButtons || updatePropsLoader_Manual || updatePropsLoader_Azure} />
|
||||
<PrimaryButton text={strings.BtnAzureProps} onClick={this._getAzurePropertyTable}
|
||||
disabled={disablePropsButtons || updatePropsLoader_Manual || updatePropsLoader_Azure} />
|
||||
{showPropsLoader && <Spinner className={styles.generateTemplateLoader} label={strings.PropsLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
}
|
||||
{manualPropertyData && manualPropertyData.length > 0 &&
|
||||
<ManualPropertyUpdate userProperties={manualPropertyData} UpdateSPUserWithManualProps={this._updateSPWithManualProperties}
|
||||
showProgress={updatePropsLoader_Manual} />
|
||||
}
|
||||
{azurePropertyData && azurePropertyData.length > 0 &&
|
||||
<AzurePropertyView userProperties={azurePropertyData} UpdateSPUserWithAzureProps={this._updateSPWithAzureProperties}
|
||||
showProgress={updatePropsLoader_Azure} />
|
||||
}
|
||||
{clearData &&
|
||||
<div><MessageContainer MessageScope={MessageScope.Success} Message={strings.JobIntializedSuccess} /></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{selectedMenu == "1" &&
|
||||
<div className={css(styles.menuContent)}>
|
||||
<div>
|
||||
<FilePicker
|
||||
accepts={[".json", ".csv"]}
|
||||
buttonIcon="FileImage"
|
||||
onSave={this._onSaveTemplate}
|
||||
onChanged={this._onChangeTemplate}
|
||||
context={this.props.context}
|
||||
disabled={showUploadProgress || updatePropsLoader_Bulk || noActivePropertyMappings}
|
||||
buttonLabel={"Select Data file"}
|
||||
hideLinkUploadTab={true}
|
||||
hideOrganisationalAssetTab={true}
|
||||
hideWebSearchTab={true}
|
||||
/>
|
||||
</div>
|
||||
{fileurl &&
|
||||
<div style={{ color: "black", padding: '10px' }}>
|
||||
<FileTypeIcon type={IconType.font} path={fileurl} /> {uploadedTemplate.fileName}
|
||||
</div>
|
||||
}
|
||||
{showUploadData &&
|
||||
<div style={{ padding: '10px', width: 'auto', display: 'inline-block' }}>
|
||||
<PrimaryButton text={strings.BtnUploadDataForSync} onClick={this._uploadDataToSync} disabled={showUploadProgress || updatePropsLoader_Bulk} />
|
||||
{showUploadProgress &&
|
||||
<div style={{ paddingLeft: '10px', display: 'inline-block' }}><Spinner className={styles.generateTemplateLoader} label={strings.UploadDataToSyncLoader} ariaLive="assertive" labelPosition="right" /></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<UPPropertyData items={uploadedData} isCSV={isCSV} UpdateSPForBulkUsers={this._updateSPForBulkUsers} showProgress={updatePropsLoader_Bulk}
|
||||
clearData={clearData} />
|
||||
{clearData &&
|
||||
<div><MessageContainer MessageScope={MessageScope.Success} Message={strings.JobIntializedSuccess} /></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{selectedMenu == "2" &&
|
||||
<div className={css(styles.menuContent)}>
|
||||
<BulkSyncList helper={this.state.helper} siteurl={this.props.context.pageContext.web.serverRelativeUrl} dateFormat={this.props.dateFormat} />
|
||||
</div>
|
||||
}
|
||||
{selectedMenu == "3" &&
|
||||
<div className={css(styles.menuContent)}>
|
||||
<TemplatesView helper={this.state.helper} siteurl={this.props.context.pageContext.web.serverRelativeUrl} dateFormat={this.props.dateFormat} />
|
||||
</div>
|
||||
}
|
||||
{selectedMenu == "4" &&
|
||||
<div className={css(styles.menuContent)}>
|
||||
<SyncJobsView helper={this.state.helper} dateFormat={this.props.dateFormat} />
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
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 { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
import { css } from 'office-ui-fabric-react/lib';
|
||||
import SPHelper from '../../../../Common/SPHelper';
|
||||
import MessageContainer from '../MessageContainer';
|
||||
import { MessageScope } from '../../../../Common/IModel';
|
||||
|
||||
const map: any = require('lodash/map');
|
||||
const union: any = require('lodash/union');
|
||||
|
||||
export interface ISyncJobResultsProps {
|
||||
helper: SPHelper;
|
||||
data: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export default function SyncJobResults(props: ISyncJobResultsProps) {
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [jobresults, setJobResults] = React.useState<any[]>([]);
|
||||
const [columns, setColumns] = React.useState<IColumn[]>([]);
|
||||
|
||||
const _buildColumns = (colValues: string[]) => {
|
||||
let cols: IColumn[] = [];
|
||||
colValues.map(col => {
|
||||
if (col.toLowerCase() == "userid") {
|
||||
cols.push({
|
||||
key: 'UserID', name: 'User ID', fieldName: col, minWidth: 250, maxWidth: 250,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: `/_layouts/15/userphoto.aspx?Size=S&Username=${item[col]}`,
|
||||
text: item[col],
|
||||
};
|
||||
return (
|
||||
<div><Persona {...authorPersona} size={PersonaSize.size24} /></div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
} else {
|
||||
cols.push({
|
||||
key: col, name: col, fieldName: col, minWidth: 100, maxWidth: 250,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (
|
||||
<>
|
||||
{(item[col] && item[col] != '') ? (
|
||||
<>{item[col]} - <span className={css(styles.resultsIconSpan, styles.status, styles.green)}><Icon iconName="Completed" /></span></>
|
||||
) : (
|
||||
<>{"Empty"} - <span className={css(styles.resultsIconSpan, styles.status, styles.red)}><Icon iconName="ErrorBadge" /></span></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
}
|
||||
});
|
||||
setColumns(cols);
|
||||
};
|
||||
|
||||
const _buildJobResults = () => {
|
||||
if (props.error && props.error.length > 0) {
|
||||
|
||||
} else {
|
||||
let parsedResults = JSON.parse(props.data);
|
||||
let colValues = ['UserID'];
|
||||
colValues = union(colValues, map(parsedResults.value[0].properties, 'name'));
|
||||
_buildColumns(colValues);
|
||||
let users = [];
|
||||
map(parsedResults.value, (userProps) => {
|
||||
var obj = new Object();
|
||||
obj['UserID'] = userProps.userid;
|
||||
map(userProps.properties, (prop) => {
|
||||
obj[prop.name] = prop.value;
|
||||
});
|
||||
users.push(obj);
|
||||
});
|
||||
setJobResults(users);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
React.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}
|
||||
className={styles.uppropertylist} />
|
||||
}
|
||||
{props.error && props.error.length > 0 &&
|
||||
<MessageContainer MessageScope={MessageScope.Failure} Message={`${strings.SyncFailedErrorMessage} ${props.error}`} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
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 { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
|
||||
import { Icon, IIconProps } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { ActionButton, IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { css } from 'office-ui-fabric-react/lib';
|
||||
import SPHelper from '../../../../Common/SPHelper';
|
||||
import * as moment from 'moment/moment';
|
||||
import MessageContainer from '../MessageContainer';
|
||||
import { MessageScope } from '../../../../Common/IModel';
|
||||
import SyncJobResults from './SyncJobResults';
|
||||
|
||||
const orderBy: any = require('lodash/orderBy');
|
||||
const filter: any = require('lodash/filter');
|
||||
|
||||
export interface ISyncJobsProps {
|
||||
helper: SPHelper;
|
||||
dateFormat: string;
|
||||
}
|
||||
|
||||
export default function SyncJobsView(props: ISyncJobsProps) {
|
||||
const actionIcon: IIconProps = { iconName: 'InfoSolid' };
|
||||
const refreshIcon: IIconProps = { iconName: 'Refresh' };
|
||||
|
||||
const [refreshLoading, setRefreshLoading] = React.useState<boolean>(false);
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [jobs, setJobs] = React.useState<any[]>([]);
|
||||
const [filteredjobs, setFilteredJobs] = React.useState<any[]>([]);
|
||||
const [columns, setColumns] = React.useState<IColumn[]>([]);
|
||||
const [jobresults, setJobResults] = React.useState<string>('');
|
||||
const [errorMsg, setErrorMessage] = React.useState<string>('');
|
||||
const [hideDialog, setHideDialog] = React.useState<boolean>(true);
|
||||
const [searchKey, setSearchKey] = React.useState<string>('');
|
||||
const [emptySearch, setEmptySearch] = React.useState<boolean>(false);
|
||||
|
||||
const actionClick = (data) => {
|
||||
setJobResults(data.SyncResults);
|
||||
setErrorMessage(data.ErrorMessage);
|
||||
setHideDialog(false);
|
||||
};
|
||||
const StatusRender = (childprops) => {
|
||||
switch (childprops.Status.toLowerCase()) {
|
||||
case 'submitted':
|
||||
return (<div className={css(styles.status, styles.blue)}><Icon iconName="Save" /> {childprops.Status}</div>);
|
||||
case 'in-progress':
|
||||
return (<div className={css(styles.status, styles.orange)}><Icon iconName="ProgressRingDots" /> {childprops.Status}</div>);
|
||||
case 'completed':
|
||||
return (<div className={css(styles.status, styles.green)}><Icon iconName="Completed" /> {childprops.Status}</div>);
|
||||
case 'error':
|
||||
case 'completed with error':
|
||||
return (<div className={css(styles.status, styles.red)}><Icon iconName="ErrorBadge" /> {childprops.Status}</div>);
|
||||
}
|
||||
};
|
||||
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 } as IColumn);
|
||||
cols.push({ key: 'Title', name: 'Title', fieldName: 'Title', minWidth: 250, maxWidth: 250 } as IColumn);
|
||||
cols.push({ key: 'SyncType', name: 'Sync Type', fieldName: 'SyncType', minWidth: 150, maxWidth: 150 } as IColumn);
|
||||
cols.push({
|
||||
key: 'Author', name: 'Author', fieldName: 'Author.Title', minWidth: 250, maxWidth: 250,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: `/_layouts/15/userphoto.aspx?Size=S&Username=${item["Author"].EMail}`,
|
||||
text: item["Author"].Title,
|
||||
};
|
||||
return (
|
||||
<div><Persona {...authorPersona} size={PersonaSize.size24} /></div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Created', name: 'Created', fieldName: 'Created', minWidth: 150, maxWidth: 150,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (<div>{moment(item.Created).format(props.dateFormat)}</div>);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Status', name: 'Status', fieldName: 'Status', minWidth: 200, maxWidth: 200,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (<StatusRender Status={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 _loadJobsList = async () => {
|
||||
let jobslist = await props.helper.getAllJobs();
|
||||
jobslist = orderBy(jobslist, ['ID'], ['desc']);
|
||||
setJobs(jobslist);
|
||||
};
|
||||
const _buildJobsList = async () => {
|
||||
_buildColumns();
|
||||
await _loadJobsList();
|
||||
setLoading(false);
|
||||
};
|
||||
const _closeDialog = () => {
|
||||
setHideDialog(true);
|
||||
};
|
||||
const _refreshList = async () => {
|
||||
setRefreshLoading(true);
|
||||
await _loadJobsList();
|
||||
setRefreshLoading(false);
|
||||
};
|
||||
const _searchJobsList = (srchkey) => {
|
||||
setEmptySearch(false);
|
||||
setSearchKey(srchkey);
|
||||
let filteredList = filter(jobs, (o) => {
|
||||
return o.Title.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0 || o['Author'].Title.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0 ||
|
||||
o.Status.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0 || o.SyncType.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0;
|
||||
});
|
||||
if (filteredList.length <= 0) setEmptySearch(true);
|
||||
setFilteredJobs(filteredList);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
_buildJobsList();
|
||||
}, [props.dateFormat]);
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.syncjobsContainer}>
|
||||
{loading &&
|
||||
<ProgressIndicator label={strings.PropsLoader} description={strings.JobsListLoaderDesc} />
|
||||
}
|
||||
{(!loading && jobs && jobs.length > 0) ? (
|
||||
<>
|
||||
<div className={styles.searchcontainer}>
|
||||
<SearchBox placeholder={strings.JobsListSearchPH} underlined={true} value={searchKey} onChange={_searchJobsList} />
|
||||
</div>
|
||||
<div className={styles.refreshContainer}>
|
||||
<IconButton iconProps={refreshIcon} title="Refresh" ariaLabel="Refresh" onClick={_refreshList} disabled={refreshLoading} />
|
||||
{refreshLoading &&
|
||||
<Spinner size={SpinnerSize.small} />
|
||||
}
|
||||
</div>
|
||||
{emptySearch &&
|
||||
<div style={{ marginTop: '-10px', width: '95%' }}>
|
||||
<MessageContainer MessageScope={MessageScope.Failure} Message={strings.EmptySearchResults} />
|
||||
</div>
|
||||
}
|
||||
<DetailsList
|
||||
items={filteredjobs && filteredjobs.length > 0 ? filteredjobs : jobs}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true}
|
||||
className={styles.uppropertylist} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!loading &&
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.EmptyTable} />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<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 helper={props.helper} data={jobresults} error={errorMsg} />
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { FileContentType } from '../../../../Common/IModel';
|
||||
import SPHelper from '../../../../Common/SPHelper';
|
||||
|
||||
export interface ITemplatesStructureProps {
|
||||
helper: SPHelper;
|
||||
fileurl: string;
|
||||
}
|
||||
|
||||
export default function TemplateStructure(props: ITemplatesStructureProps) {
|
||||
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [userprops, setUserProps] = React.useState<any[]>([]);
|
||||
|
||||
const _loadTemplateStructure = async () => {
|
||||
let fileextn: string = props.fileurl.split('.').pop();
|
||||
let filecontent: any = null;
|
||||
let finaljson: any[] = [];
|
||||
if (fileextn.toLowerCase() === "csv") {
|
||||
let csvcontent = await props.helper.getFileContent(props.fileurl, FileContentType.Text);
|
||||
let re = /\"/gi;
|
||||
csvcontent.split(',').map((prop: string) => {
|
||||
finaljson.push(prop.replace(re, ''));
|
||||
});
|
||||
} else if (fileextn.toLowerCase() === "json") {
|
||||
filecontent = await props.helper.getFileContent(props.fileurl, FileContentType.JSON);
|
||||
Object.keys(filecontent[0]).map((key) => {
|
||||
finaljson.push(key);
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
setUserProps(finaljson);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
_loadTemplateStructure();
|
||||
}, [props.fileurl]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading &&
|
||||
<Spinner size={SpinnerSize.small} label={strings.TemplatePropsLoaderDesc} labelPosition={"top"} />
|
||||
}
|
||||
{!loading && userprops && userprops.length > 0 &&
|
||||
<div className={styles.propertyMappingContainer} data-is-focusable={true}>
|
||||
{userprops.map((userprop: string) => {
|
||||
return <div className={styles.propertydiv}>{userprop}</div>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
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 { 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 { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { Link } from 'office-ui-fabric-react/lib/Link';
|
||||
import { Icon, IconType, IIconProps } from 'office-ui-fabric-react/lib/Icon';
|
||||
import * as moment from 'moment/moment';
|
||||
import SPHelper from '../../../../Common/SPHelper';
|
||||
import MessageContainer from '../MessageContainer';
|
||||
import { MessageScope, FileContentType } from '../../../../Common/IModel';
|
||||
import TemplateStructure from './TemplatesStructure';
|
||||
|
||||
const orderBy: any = require('lodash/orderBy');
|
||||
const filter: any = require('lodash/filter');
|
||||
|
||||
export interface ITemplatesProps {
|
||||
helper: SPHelper;
|
||||
siteurl: string;
|
||||
dateFormat: string;
|
||||
}
|
||||
|
||||
export default function TemplatesView(props: ITemplatesProps) {
|
||||
const actionIcon: IIconProps = { iconName: 'InfoSolid' };
|
||||
const refreshIcon: IIconProps = { iconName: 'Refresh' };
|
||||
|
||||
const [refreshLoading, setRefreshLoading] = React.useState<boolean>(false);
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [downloadLoading, setDownloadLoading] = React.useState<boolean>(false);
|
||||
const [templates, setTemplates] = React.useState<any[]>([]);
|
||||
const [filteredtemplates, setFilteredTemplates] = React.useState<any[]>([]);
|
||||
const [columns, setColumns] = React.useState<IColumn[]>([]);
|
||||
const [searchKey, setSearchKey] = React.useState<string>('');
|
||||
const [emptySearch, setEmptySearch] = React.useState<boolean>(false);
|
||||
const [hideDialog, setHideDialog] = React.useState<boolean>(true);
|
||||
const [fileurl, setFileUrl] = React.useState<string>('');
|
||||
|
||||
const downloadTemplate = async (fileserverurl, filename) => {
|
||||
setDownloadLoading(true);
|
||||
const anchor = window.document.createElement('a');
|
||||
anchor.href = `${props.siteurl}/_layouts/15/download.aspx?SourceUrl=${fileserverurl}`;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
setDownloadLoading(false);
|
||||
};
|
||||
const actionClick = async (data) => {
|
||||
if (data.FileUrl) {
|
||||
setFileUrl(data.FileUrl);
|
||||
setHideDialog(false);
|
||||
}
|
||||
};
|
||||
const ActionRender = (actionProps) => {
|
||||
return (
|
||||
<ActionButton iconProps={actionIcon} allowDisabledFocus onClick={() => { actionClick(actionProps); }} />
|
||||
);
|
||||
};
|
||||
const _buildColumns = () => {
|
||||
let cols: IColumn[] = [];
|
||||
cols.push({
|
||||
key: 'Name', name: 'Name', fieldName: 'Name', minWidth: 300, maxWidth: 300,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
let fileextn = item.Name.split('.').pop();
|
||||
return (
|
||||
<>
|
||||
<div className={styles.fileiconDiv}>
|
||||
{fileextn.toLowerCase() === "csv" &&
|
||||
<Icon iconName="ExcelDocument" ariaLabel={item.Name} iconType={IconType.Default} />
|
||||
}
|
||||
{fileextn.toLowerCase() === "json" &&
|
||||
<Icon iconName="FileCode" ariaLabel={item.Name} iconType={IconType.Default} />
|
||||
}
|
||||
</div>
|
||||
<Link onClick={() => { downloadTemplate(`${item.ServerRelativeUrl}`, `${item.Name}`); }} value={item.Name}>{item.Name}</Link>
|
||||
{downloadLoading &&
|
||||
<div className={styles.downloadLoaderDiv}>
|
||||
<Spinner size={SpinnerSize.small} />
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
cols.push({
|
||||
key: 'Author', name: 'Author', fieldName: 'Author', minWidth: 300, maxWidth: 300,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
const authorPersona: IPersonaSharedProps = {
|
||||
imageUrl: `/_layouts/15/userphoto.aspx?Size=S&Username=${item["Author"].Email}`,
|
||||
text: item["Author"].Title,
|
||||
};
|
||||
return (
|
||||
<div><Persona {...authorPersona} size={PersonaSize.size24} /></div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'TimeCreated', name: 'Created', fieldName: 'TimeCreated', minWidth: 100, maxWidth: 200,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (
|
||||
<div>{moment(item.TimeCreated).format(props.dateFormat)}</div>
|
||||
);
|
||||
}
|
||||
} as IColumn);
|
||||
cols.push({
|
||||
key: 'Actions', name: 'Actions', fieldName: 'ID', minWidth: 100, maxWidth: 100,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
return (<ActionRender FileUrl={item.ServerRelativeUrl} />);
|
||||
}
|
||||
});
|
||||
setColumns(cols);
|
||||
};
|
||||
const _loadTemplatesList = async () => {
|
||||
let templateList = await props.helper.getAllTemplates();
|
||||
templateList = orderBy(templateList, ['TimeCreated'], ['desc']);
|
||||
setTemplates(templateList);
|
||||
};
|
||||
const _buildTemplatesList = async () => {
|
||||
_buildColumns();
|
||||
await _loadTemplatesList();
|
||||
setLoading(false);
|
||||
};
|
||||
const _refreshList = async () => {
|
||||
setRefreshLoading(true);
|
||||
await _loadTemplatesList();
|
||||
setRefreshLoading(false);
|
||||
};
|
||||
const _searchTemplatesList = (srchkey) => {
|
||||
setEmptySearch(false);
|
||||
setSearchKey(srchkey);
|
||||
let filteredList = filter(templates, (o) => {
|
||||
return o.Name.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0 || o['Author'].Title.toLowerCase().indexOf(srchkey.toLowerCase()) >= 0;
|
||||
});
|
||||
if (filteredList.length <= 0) setEmptySearch(true);
|
||||
setFilteredTemplates(filteredList);
|
||||
};
|
||||
const _closeDialog = () => {
|
||||
setHideDialog(true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
_buildTemplatesList();
|
||||
}, [props.dateFormat]);
|
||||
|
||||
return (
|
||||
<div className={styles.templatesContainer}>
|
||||
{loading &&
|
||||
<ProgressIndicator label={strings.PropsLoader} description={strings.TemplateListLoaderDesc} />
|
||||
}
|
||||
{(!loading && templates && templates.length > 0) ? (
|
||||
<>
|
||||
<div className={styles.searchcontainer}>
|
||||
<SearchBox placeholder={strings.TemplateListSearchPH} underlined={true} value={searchKey} onChange={_searchTemplatesList} />
|
||||
</div>
|
||||
<div className={styles.refreshContainer}>
|
||||
<IconButton iconProps={refreshIcon} title="Refresh" ariaLabel="Refresh" onClick={_refreshList} disabled={refreshLoading} />
|
||||
{refreshLoading &&
|
||||
<Spinner size={SpinnerSize.small} />
|
||||
}
|
||||
</div>
|
||||
{emptySearch &&
|
||||
<div style={{ marginTop: '-10px', width: '95%' }}>
|
||||
<MessageContainer MessageScope={MessageScope.Failure} Message={strings.EmptySearchResults} />
|
||||
</div>
|
||||
}
|
||||
<div className={styles.templateList}>
|
||||
<DetailsList
|
||||
items={filteredtemplates && filteredtemplates.length > 0 ? filteredtemplates : templates}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true}
|
||||
className={styles.uppropertylist} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!loading &&
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.EmptyTable} />
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Dialog hidden={hideDialog} onDismiss={_closeDialog} maxWidth='700' minWidth='500px'
|
||||
dialogContentProps={{
|
||||
type: DialogType.close,
|
||||
title: `${strings.TemplateStructureDialogTitle}`
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
isDarkOverlay: true,
|
||||
styles: { main: { minWidth: 500, maxHeight: 700 } },
|
||||
}}>
|
||||
<TemplateStructure helper={props.helper} fileurl={fileurl} />
|
||||
</Dialog>
|
||||
</div >
|
||||
);
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
import * as React from 'react';
|
||||
import styles from './SpupsProperySync.module.scss';
|
||||
import * as strings from 'SpupsProperySyncWebPartStrings';
|
||||
import { DetailsList, IColumn, DetailsListLayoutMode, ConstrainMode, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import * as csv from 'csvtojson';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import { MessageScope } from '../../../Common/IModel';
|
||||
|
||||
export interface IUPPropertyDataProps {
|
||||
items: any;
|
||||
isCSV: boolean;
|
||||
showProgress: boolean;
|
||||
clearData: boolean;
|
||||
UpdateSPForBulkUsers: (data: any[]) => void;
|
||||
}
|
||||
|
||||
export interface IUPPropertyDataState {
|
||||
items: any;
|
||||
columns: IColumn[];
|
||||
dynamicColumns: string[];
|
||||
searchText: string;
|
||||
emptyValues: boolean;
|
||||
}
|
||||
|
||||
export default class UPPropertyData extends React.Component<IUPPropertyDataProps, IUPPropertyDataState> {
|
||||
constructor(props: IUPPropertyDataProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
items: [],
|
||||
columns: [],
|
||||
searchText: '',
|
||||
dynamicColumns: [],
|
||||
emptyValues: false
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount = () => {
|
||||
this._buildUploadDataList();
|
||||
}
|
||||
|
||||
public componentDidUpdate = (prevProps: IUPPropertyDataProps) => {
|
||||
if (prevProps.items !== this.props.items || prevProps.isCSV !== this.props.isCSV) {
|
||||
this._buildUploadDataList();
|
||||
}
|
||||
if (prevProps.clearData !== this.props.clearData) {
|
||||
if (this.props.clearData) this.setState({ items: [] });
|
||||
}
|
||||
}
|
||||
|
||||
private _buildColumns = (columns: string[]): IColumn[] => {
|
||||
this.setState({ emptyValues: false });
|
||||
let cols: IColumn[] = [];
|
||||
if (columns && columns.length > 0) {
|
||||
columns.map((col: string) => {
|
||||
if (col.toLocaleLowerCase() == "userid") {
|
||||
cols.push({ key: col, name: col, fieldName: col, minWidth: 300, maxWidth: 300 } as IColumn);
|
||||
} else {
|
||||
cols.push({
|
||||
key: col, name: col, fieldName: col, minWidth: 150,
|
||||
onRender: (item: any, index: number, column: IColumn) => {
|
||||
if (item[col]) {
|
||||
return (<div>{item[col]}</div>);
|
||||
} else {
|
||||
this.setState({ emptyValues: true });
|
||||
return (<div className={styles.emptyData}>{strings.EmptyDataText}</div>);
|
||||
}
|
||||
}
|
||||
} as IColumn);
|
||||
}
|
||||
});
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
private _buildUploadDataList = async () => {
|
||||
const { items, isCSV } = this.props;
|
||||
if (items) {
|
||||
if (isCSV) {
|
||||
let finalOut: any = await csv().fromString(items);
|
||||
this._getJSONData(finalOut);
|
||||
}
|
||||
else this._getJSONData(items);
|
||||
}
|
||||
}
|
||||
|
||||
private _getJSONData = (inputjson?: any) => {
|
||||
let parsedJson = (inputjson) ? inputjson : JSON.parse(inputjson);
|
||||
let _dynamicColumns: string[] = [];
|
||||
Object.keys(parsedJson[0]).map((key) => {
|
||||
_dynamicColumns.push(key);
|
||||
});
|
||||
this.setState({
|
||||
columns: this._buildColumns(_dynamicColumns),
|
||||
items: parsedJson
|
||||
});
|
||||
}
|
||||
|
||||
private _updatePropsForBulkUsers = () => {
|
||||
this.props.UpdateSPForBulkUsers(this.state.items);
|
||||
this.setState({ emptyValues: false });
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { items, columns, emptyValues } = this.state;
|
||||
return (
|
||||
<div className={styles.uppropertydata}>
|
||||
{emptyValues && !this.props.clearData &&
|
||||
<MessageContainer MessageScope={MessageScope.Info} Message={strings.EmptyDataWarningMsg} />
|
||||
}
|
||||
{(items && items.length > 0) ? (
|
||||
<>
|
||||
<DetailsList
|
||||
items={items}
|
||||
setKey="set"
|
||||
columns={columns}
|
||||
compact={true}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
isHeaderVisible={true}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={true}
|
||||
className={styles.uppropertylist} />
|
||||
<div style={{ padding: "10px" }}>
|
||||
<PrimaryButton text={strings.BtnUpdateUserProps} onClick={this._updatePropsForBulkUsers} style={{ marginRight: '5px' }} disabled={this.props.showProgress} />
|
||||
{this.props.showProgress && <Spinner className={styles.generateTemplateLoader} label={strings.PropsUpdateLoader} ariaLive="assertive" labelPosition="right" />}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
define([], function() {
|
||||
return {
|
||||
PropertyPaneDescription: "",
|
||||
BasicGroupName: "Configurations",
|
||||
ListCreationText: "Verifying the required list and loading the properties...",
|
||||
PropTemplateLibLabel: "Select a library to store the templates",
|
||||
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",
|
||||
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.",
|
||||
|
||||
PlaceholderIconText: "Configure the settings",
|
||||
PlaceholderDescription: "Use the configuration settings to map the document library required to store the property mapping templates.",
|
||||
PlaceholderButtonLabel: "Configure",
|
||||
DefaultAppTitle: "SharePoint Profile Property Sync",
|
||||
JobResultsDialogTitle: "Users list with properties updated!",
|
||||
JobsListSearchPH: "Search by Title, SyncType, Author, Status...",
|
||||
TemplateListSearchPH: "Search by Name, Author...",
|
||||
TemplateStructureDialogTitle: "Properties defined in the template!",
|
||||
BulkSyncDataDialogTitle: "Data defined in the file!",
|
||||
|
||||
GenerateTemplateLoader: "Wait, generating the template...",
|
||||
UploadDataToSyncLoader: "Wait, uploading data for syncing",
|
||||
PropsLoader: "Please wait...",
|
||||
PropsUpdateLoader: "Please wait, initializing the job to update the properties",
|
||||
JobsListLoaderDesc: "Loading the jobs list...",
|
||||
JobResultsLoaderDesc: "Loading the results...",
|
||||
TemplateListLoaderDesc: "Loading the templates...",
|
||||
TemplatePropsLoaderDesc: "Loading properties, please wait...",
|
||||
BulkSyncListLoaderDesc: "Loading the bulk sync files...",
|
||||
BulkSyncFileDataLoaderDesc: "Loading data, please wait...",
|
||||
AccessCheckDesc: "Checking for access...",
|
||||
SitePrivilegeCheckLabel: "Checking site admin privilege...",
|
||||
|
||||
BtnGenerateJSON: "Generate JSON",
|
||||
BtnGenerateCSV: "Generate CSV",
|
||||
BtnSaveForManual: "Save for Manual Update",
|
||||
BtnPropertyMapping: "Property Mapping",
|
||||
BtnUploadDataForSync: "Upload Data to Sync",
|
||||
BtnUpdateUserProps: "Update User Properties",
|
||||
BtnManualProps: "Initialize Manual Properties",
|
||||
BtnAzureProps: "Get Azure Properties",
|
||||
|
||||
PnlHeaderText: "Property Mappings",
|
||||
TblColHeadAzProperty: "Azure Property",
|
||||
TblColHeadSPProperty: "SharePoint Property",
|
||||
TblColHeadEnableDisable: "Enabled/Disabled",
|
||||
PPLPickerTitleText: "Select users to update their properties",
|
||||
|
||||
EmptyPropertyMappings: "No active property mappings found. Please navigate to 'Sync Properties Mapping' list or contact your administrator to activate the properties.",
|
||||
TemplateDownloaded: "Please use the downloaded file to update the User properties!",
|
||||
EmptyDataText: "Empty!",
|
||||
EmptyDataWarningMsg: "Columns with empty values are not considered for update!",
|
||||
EmptyTable: "Sorry, no data to be displayed!",
|
||||
EmptyFile: "Oops, the file is empty",
|
||||
EmptySearchResults: "Sorry, no data found. Displaying all the data",
|
||||
UserListChanges: "Changes in user list, please remove the user from the table manually or reinitialize or get the Azure properties again!",
|
||||
UserListEmpty: "Since all the users have been removed, the table has been cleared!",
|
||||
JobIntializedSuccess: "Property sync job has been initialized. Track the status of the job on the 'Sync Jobs' tab!",
|
||||
AdminConfigHelp: "Please contact your site administrator to configure the webpart.",
|
||||
AccessDenied: "Access denied. Please contact your administrator.",
|
||||
SyncFailedErrorMessage: "Oops, there is an error while updating the properties. Error Message:",
|
||||
|
||||
TabMenu1: "Manual or Azure Property Sync",
|
||||
TabMenu2: "Bulk Sync",
|
||||
TabMenu3: "Bulk Files Uploaded",
|
||||
TabMenu4: "Templates Generated",
|
||||
TabMenu5: "Sync Status"
|
||||
}
|
||||
});
|
79
samples/react-spupsproperty-sync/src/webparts/spupsProperySync/loc/mystrings.d.ts
vendored
Normal file
79
samples/react-spupsproperty-sync/src/webparts/spupsProperySync/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
declare interface ISpupsProperySyncWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
ListCreationText: string;
|
||||
PropTemplateLibLabel: string;
|
||||
PropAzFuncLabel: string;
|
||||
PropAzFuncDesc: string;
|
||||
PropUseCertLabel: string;
|
||||
PropUseCertCallout: string;
|
||||
PropDateFormatLabel: string;
|
||||
PropInfoDateFormat: string;
|
||||
PropInfoUseCert: string;
|
||||
PropInfoTemplateLib: string;
|
||||
PropInfoNormalUser: string;
|
||||
PropAllowedUserInfo: string;
|
||||
|
||||
PlaceholderIconText: string;
|
||||
PlaceholderDescription: string;
|
||||
PlaceholderButtonLabel: string;
|
||||
DefaultAppTitle: string;
|
||||
JobResultsDialogTitle: string;
|
||||
JobsListSearchPH: string;
|
||||
TemplateListSearchPH: string;
|
||||
TemplateStructureDialogTitle: string;
|
||||
BulkSyncDataDialogTitle: string;
|
||||
BulkSyncFileDataLoaderDesc: string;
|
||||
|
||||
GenerateTemplateLoader: string;
|
||||
UploadDataToSyncLoader: string;
|
||||
PropsLoader: string;
|
||||
PropsUpdateLoader: string;
|
||||
JobsListLoaderDesc: string;
|
||||
JobResultsLoaderDesc: string;
|
||||
TemplateListLoaderDesc: string;
|
||||
TemplatePropsLoaderDesc: string;
|
||||
BulkSyncListLoaderDesc: string;
|
||||
AccessCheckDesc: string;
|
||||
SitePrivilegeCheckLabel: string;
|
||||
|
||||
BtnGenerateJSON: string;
|
||||
BtnGenerateCSV: string;
|
||||
BtnSaveForManual: string;
|
||||
BtnPropertyMapping: string;
|
||||
BtnUploadDataForSync: string;
|
||||
BtnUpdateUserProps: string;
|
||||
BtnManualProps: string;
|
||||
BtnAzureProps: string;
|
||||
|
||||
PnlHeaderText: string;
|
||||
TblColHeadAzProperty: string;
|
||||
TblColHeadSPProperty: string;
|
||||
TblColHeadEnableDisable: string;
|
||||
PPLPickerTitleText: string;
|
||||
|
||||
EmptyPropertyMappings: string;
|
||||
TemplateDownloaded: string;
|
||||
EmptyDataText: string;
|
||||
EmptyDataWarningMsg: string;
|
||||
EmptyTable: string;
|
||||
EmptyFile: string;
|
||||
EmptySearchResults: string;
|
||||
UserListChanges: string;
|
||||
UserListEmpty: string;
|
||||
JobIntializedSuccess: string;
|
||||
AdminConfigHelp: string;
|
||||
AccessDenied: string;
|
||||
SyncFailedErrorMessage: string;
|
||||
|
||||
TabMenu1: string;
|
||||
TabMenu2: string;
|
||||
TabMenu3: string;
|
||||
TabMenu4: string;
|
||||
TabMenu5: string;
|
||||
}
|
||||
|
||||
declare module 'SpupsProperySyncWebPartStrings' {
|
||||
const strings: ISpupsProperySyncWebPartStrings;
|
||||
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