Merge pull request #2 from pnp/master

This commit is contained in:
João Mendes 2020-07-29 11:28:55 +01:00 committed by GitHub
commit 726bf8a47a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 23123 additions and 3 deletions

View File

@ -51,6 +51,7 @@ Version|Date|Comments
1.1|February 05, 2020|Update to SPFx 1.10.0
1.2|June 04, 2020|Added full-width support
1.3|July 07, 2020|Simplified web part
1.4|July 28, 2020|Update styles to minimise toolbar overlap
## Disclaimer

View File

@ -3,7 +3,7 @@
"solution": {
"name": "workbench-customizer-client-side-solution",
"id": "5d6f4a5a-9d2b-4a93-a283-16b8f5ea75d6",
"version": "1.3.0.0",
"version": "1.4.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},

View File

@ -1,6 +1,6 @@
{
"name": "workbench-customizer",
"version": "0.0.1",
"version": "1.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "workbench-customizer",
"version": "1.3.0",
"version": "1.4.0",
"private": true,
"main": "lib/index.js",
"engines": {

View File

@ -11,6 +11,10 @@
.ControlZone {
padding: 0;
.CanvasControlToolbar {
left: -32px;
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,34 @@
{
"@pnp/generator-spfx": {
"framework": "react",
"pnpFramework": "reactjs.plus",
"pnp-libraries": [
"jquery@3",
"msgraph",
"@pnp/pnpjs",
"@pnp/spfx-property-controls",
"spfx-uifabric-themes",
"lodash",
"@pnp/spfx-controls-react",
"ouifr@7",
"rush@3.7"
],
"pnp-ci": "azure-preview",
"pnp-vetting": [],
"spfxenv": "spo",
"pnp-testing": [
"jest"
]
},
"@microsoft/generator-sharepoint": {
"environment": "spo",
"framework": "react",
"isCreatingSolution": true,
"version": "1.10.0",
"libraryName": "react-manage-profile-card-properties",
"libraryId": "a53e4c14-1c0e-4a54-810f-eb5cdf42005b",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,72 @@
# React Manage Profile Card Properties
## Summary
This web part allows tenant administrators to manage profile card properties.
We can create, edit, delete, directory properties that can be add to profile card.
Only users with Tenant Admin Role are allowed to managed profile card properties.
## Manage Profile Card Properties
![manageProps](assets/Screenshot1.png)
![manageProps](assets/Screenshot2.png)
![manageProps](assets/Screenshot3.png)
![manageProps](assets/Screenshot4.png)
![manageProps](assets/Screenshot5.png)
## Used SharePoint Framework Version
![SPFx 1.11.0](https://img.shields.io/badge/version-1.11.0-green.svg)
## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## WebPart Properties
Property |Type|Required| comments
--------------------|----|--------|----------
WebPart Title| Text| no|
## Solution
The Web Part Use Microsoft Graph API, Fluent-UI components.
Solution|Author(s)
--------|---------
Manage Profile Card Properties |João Mendes
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|July 28, 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
- Move to sample folder
- In the command line run:
- `npm install`
- `gulp build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- `Add to AppCatalog and deploy`
- Approve the required permissions (User.ReadWrite, Directory.AccessAsUser.All) on SharePoint Admin in App permissions
> **NOTE:** This web part does not work in the local workbench
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-manage-profile-card-properties" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 KiB

View File

@ -0,0 +1,67 @@
parameters:
name: ''
jobs:
- job: ${{ parameters.name }}
pool:
vmImage: 'ubuntu-latest'
demands:
- npm
- node.js
- java
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm
steps:
- checkout: self
- task: NodeTool@0
displayName: 'Use Node 10.x'
inputs:
versionSpec: 10.x
checkLatest: true
- task: CacheBeta@1
inputs:
key: npm | $(Agent.OS) | package-lock.json
path: $(npm_config_cache)
cacheHitVar: CACHE_RESTORED
- script: npm ci
displayName: 'npm ci'
- task: Gulp@0
displayName: 'Bundle project'
inputs:
targets: bundle
arguments: '--ship'
- script: npm test
displayName: 'npm test'
- task: PublishTestResults@2
displayName: Publish test results
inputs:
testResultsFormat: JUnit
testResultsFiles: '**/junit.xml'
#failTaskOnFailedTests: true #if we want to fail the build on failed unit tests
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage results'
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
- task: Gulp@0
displayName: 'Package Solution'
inputs:
targets: 'package-solution'
arguments: '--ship'
- task: CopyFiles@2
displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)'
inputs:
Contents: |
sharepoint/**/*.sppkg
TargetFolder: '$(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'

View File

@ -0,0 +1,39 @@
parameters:
# unique name of the job
job_name: deploy_sppkg
# friendly name of the job
display_name: Upload & deploy *.sppkg to SharePoint app catalog
# name of target enviroment deploying to
target_environment: ''
# app catalog scope (tenant|sitecollection)
o365cli_app_catalog_scope: 'tenant'
variable_group_name: ''
jobs:
- deployment: ${{ parameters.job_name }}
displayName: ${{ parameters.display_name }}
pool:
vmImage: 'ubuntu-latest'
environment: ${{ parameters.target_environment }}
variables:
- group: ${{parameters.variable_group_name}} #o365_user_login, o365_user_password, o365_app_catalog_site_url
strategy:
runOnce:
deploy:
steps:
- checkout: none
- download: current
artifact: drop
patterns: '**/*.sppkg'
- script: sudo npm install --global @pnp/office365-cli
displayName: Install Office365 CLI
- script: o365 login $(o365_app_catalog_site_url) --authType password --userName $(o365_user_login) --password $(o365_user_password)
displayName: Login to Office365
- script: |
CMD_GET_SPPKG_NAME=$(find $(Pipeline.Workspace)/drop -name '*.sppkg' -exec basename {} \;)
echo "##vso[task.setvariable variable=SpPkgFileName;isOutput=true]${CMD_GET_SPPKG_NAME}"
displayName: Get generated *.sppkg filename
name: GetSharePointPackage
- script: o365 spo app add --filePath "$(Pipeline.Workspace)/drop/sharepoint/solution/$(GetSharePointPackage.SpPkgFileName)" --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }} --overwrite
displayName: Upload SharePoint package to Site Collection App Catalog
- script: o365 spo app deploy --name $(GetSharePointPackage.SpPkgFileName) --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }}
displayName: Deploy SharePoint package

View File

@ -0,0 +1,26 @@
name: $(TeamProject)_$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
resources:
- repo: self
trigger:
branches:
include:
- master
- develop
stages:
- stage: build
displayName: build
jobs:
- template: ./azure-pipelines-build-template.yml
parameters:
name: 'buildsolution'
- stage: 'deployqa'
# uncomment if you want deployments to occur only for a specific branch
#condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
jobs:
- template: ./azure-pipelines-deploy-template.yml
parameters:
job_name: deploy_solution
target_environment: 'qa'
variable_group_name: qa_configuration

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"manage-profile-card-properties-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/manageProfileCardProperties/ManageProfileCardPropertiesWebPart.js",
"manifest": "./src/webparts/manageProfileCardProperties/ManageProfileCardPropertiesWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ManageProfileCardPropertiesWebPartStrings": "lib/webparts/manageProfileCardProperties/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1 @@
{"preset":"@voitanos/jest-preset-spfx-react16","rootDir":"../src","collectCoverageFrom":["<rootDir>/**/*.{ts,tsx}","!<rootDir>/**/*.scss.*","!<rootDir>/loc/**/*.*"],"coverageReporters":["text","json","lcov","text-summary","cobertura"],"reporters":["default",["jest-junit",{"suiteName":"jest tests","outputDirectory":"temp/test/junit","outputName":"junit.xml"}]]}

View File

@ -0,0 +1,24 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-manage-profile-card-properties-client-side-solution",
"id": "a53e4c14-1c0e-4a54-810f-eb5cdf42005b",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite.All"
},
{
"resource": "Microsoft Graph",
"scope": "Directory.AccessAsUser.All"
}
]
},
"paths": {
"zippedPackage": "solution/react-manage-profile-card-properties.sppkg"
}
}

View File

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

View File

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

View File

@ -0,0 +1,29 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
// Create clean development package
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
{
"name": "react-manage-profile-card-properties",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"preversion": "node ./tools/pre-version.js",
"postversion": "gulp dist",
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
},
"dependencies": {
"@microsoft/sp-core-library": "1.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/pnpjs": "^2.0.6",
"@pnp/spfx-controls-react": "1.19.0",
"@pnp/spfx-property-controls": "1.19.0",
"@types/es6-promise": "0.0.33",
"@types/jquery": "^3.3.38",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"axios": "^0.19.2",
"jquery": "^3.5.1",
"office-ui-fabric-react": "^7.121.12",
"react": "16.8.5",
"react-dom": "16.8.5",
"react-hook-form": "^6.0.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/microsoft-graph-types": "^1.12.0",
"@microsoft/rush-stack-compiler-3.7": "^0.4.x",
"@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",
"@types/react": "^16.9.38",
"@voitanos/jest-preset-spfx-react16": "^1.3.2",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "1.0.0",
"jest": "^23.6.0",
"jest-junit": "^10.0.0",
"lodash": "^4.17.15",
"spfx-uifabric-themes": "^0.8.0",
"typescript": "~3.7.x"
},
"jest-junit": {
"output": "temp/test/junit/junit.xml",
"usePathForSuiteName": "true"
}
}

View File

@ -0,0 +1,19 @@
import React from "react";
import { WebPartContext } from "@microsoft/sp-webpart-base";
import {
IReadonlyTheme
} from "@microsoft/sp-component-base";
import { MSGraphClient } from "@microsoft/sp-http";
import { IListItem } from "../Entities/IListItem";
export interface IAppContextProps {
title: string;
webpartContext: WebPartContext;
themeVariant: IReadonlyTheme;
msGraphClient: MSGraphClient;
organizationId: string;
listItems: IListItem[];
}
export const AppContext = React.createContext<IAppContextProps>(undefined);

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#0b0b0b",
"neutralLighter": "#151515",
"neutralLight": "#252525",
"neutralQuaternaryAlt": "#2f2f2f",
"neutralQuaternary": "#373737",
"neutralTertiaryAlt": "#595959",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#000000"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#2d2c2c",
"neutralLighter": "#2c2b2b",
"neutralLight": "#2a2929",
"neutralQuaternaryAlt": "#272626",
"neutralQuaternary": "#252525",
"neutralTertiaryAlt": "#242323",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#2d2c2c"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#ecebe9",
"neutralLighter": "#e8e7e6",
"neutralLight": "#dedddc",
"neutralQuaternaryAlt": "#cfcecd",
"neutralQuaternary": "#c6c5c4",
"neutralTertiaryAlt": "#bebdbc",
"neutralTertiary": "#b5b4b2",
"neutralSecondary": "#9d9c9a",
"neutralPrimaryAlt": "#868482",
"neutralPrimary": "#252423",
"neutralDark": "#565453",
"black": "#3e3d3b",
"white": "#f3f2f1"
}

View File

@ -0,0 +1,221 @@
import { IDatePickerStrings } from "office-ui-fabric-react";
export const DayPickerStrings: IDatePickerStrings = {
months: [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
shortMonths: [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
],
days: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
],
shortDays: ["S", "M", "T", "W", "T", "F", "S"],
goToToday: "Go to today",
prevMonthAriaLabel: "Go to previous month",
nextMonthAriaLabel: "Go to next month",
prevYearAriaLabel: "Go to previous year",
nextYearAriaLabel: "Go to next year",
closeButtonAriaLabel: "Close date picker",
isRequiredErrorMessage: "Field is required.",
invalidInputErrorMessage: "Invalid date format.",
// isOutOfBoundsErrorMessage: `Date must be between ${minDate.toLocaleDateString()}-${maxDate.toLocaleDateString()}`,
};
export const languages = [
{ languageTag: "af-ZA", displayName: "Afrikaans South Africa" },
{ languageTag: "sq-AL", displayName: "Albanian Albania" },
{ languageTag: "ar-DZ", displayName: "Arabic Algeria" },
{ languageTag: "ar-BH", displayName: "Arabic Bahrain" },
{ languageTag: "ar-EG", displayName: "Arabic Egypt" },
{ languageTag: "ar-IQ", displayName: "Arabic Iraq" },
{ languageTag: "ar-JO", displayName: "Arabic Jordan" },
{ languageTag: "ar-KW", displayName: "Arabic Kuwait" },
{ languageTag: "ar-LB", displayName: "Arabic Lebanon" },
{ languageTag: "ar-LY", displayName: "Arabic Libya" },
{ languageTag: "ar-MA", displayName: "Arabic Morocco" },
{ languageTag: "ar-OM", displayName: "Arabic Oman" },
{ languageTag: "ar-QA", displayName: "Arabic Qatar" },
{ languageTag: "ar-SA", displayName: "Arabic Saudi Arabia" },
{ languageTag: "ar-SY", displayName: "Arabic Syria" },
{ languageTag: "ar-TN", displayName: "Arabic Tunisia" },
{ languageTag: "ar-AE", displayName: "Arabic United Arab Emirates" },
{ languageTag: "ar-YE", displayName: "Arabic Yemen" },
{ languageTag: "hy-AM", displayName: "Armenian Armenia" },
{ languageTag: "Cy-az-AZ", displayName: "Azeri (Cyrillic) Azerbaijan" },
{ languageTag: "Lt-az-AZ", displayName: "Azeri (Latin) Azerbaijan" },
{ languageTag: "eu-ES", displayName: "Basque Basque" },
{ languageTag: "be-BY", displayName: "Belarusian Belarus" },
{ languageTag: "bg-BG", displayName: "Bulgarian Bulgaria" },
{ languageTag: "ca-ES", displayName: "Catalan Catalan" },
{ languageTag: "zh-CN", displayName: "Chinese China" },
{ languageTag: "zh-HK", displayName: "Chinese Hong Kong SAR" },
{ languageTag: "zh-MO", displayName: "Chinese Macau SAR" },
{ languageTag: "zh-SG", displayName: "Chinese Singapore" },
{ languageTag: "zh-TW", displayName: "Chinese Taiwan" },
{ languageTag: "zh-CHS", displayName: "Chinese (Simplified)" },
{ languageTag: "zh-CHT", displayName: "Chinese (Traditional)" },
{ languageTag: "hr-HR", displayName: "Croatian Croatia" },
{ languageTag: "cs-CZ", displayName: "Czech Czech Republic" },
{ languageTag: "da-DK", displayName: "Danish Denmark" },
{ languageTag: "div-MV", displayName: "Dhivehi Maldives" },
{ languageTag: "nl-BE", displayName: "Dutch Belgium" },
{ languageTag: "nl-NL", displayName: "Dutch The Netherlands" },
{ languageTag: "en-AU", displayName: "English Australia" },
{ languageTag: "en-BZ", displayName: "English Belize" },
{ languageTag: "en-CA", displayName: "English Canada" },
{ languageTag: "en-CB", displayName: "English Caribbean" },
{ languageTag: "en-IE", displayName: "English Ireland" },
{ languageTag: "en-JM", displayName: "English Jamaica" },
{ languageTag: "en-NZ", displayName: "English New Zealand" },
{ languageTag: "en-PH", displayName: "English Philippines" },
{ languageTag: "en-ZA", displayName: "English South Africa" },
{ languageTag: "en-TT", displayName: "English Trinidad and Tobago" },
{ languageTag: "en-GB", displayName: "English United Kingdom" },
{ languageTag: "en-US", displayName: "English United States" },
{ languageTag: "en-ZW", displayName: "English Zimbabwe" },
{ languageTag: "et-EE", displayName: "Estonian Estonia" },
{ languageTag: "fo-FO", displayName: "Faroese Faroe Islands" },
{ languageTag: "fa-IR", displayName: "Farsi Iran" },
{ languageTag: "fi-FI", displayName: "Finnish Finland" },
{ languageTag: "fr-BE", displayName: "French Belgium" },
{ languageTag: "fr-CA", displayName: "French Canada" },
{ languageTag: "fr-FR", displayName: "French France" },
{ languageTag: "fr-LU", displayName: "French Luxembourg" },
{ languageTag: "fr-MC", displayName: "French Monaco" },
{ languageTag: "fr-CH", displayName: "French Switzerland" },
{ languageTag: "gl-ES", displayName: "Galician Galician" },
{ languageTag: "ka-GE", displayName: "Georgian Georgia" },
{ languageTag: "de-AT", displayName: "German Austria" },
{ languageTag: "de-DE", displayName: "German Germany" },
{ languageTag: "de-LI", displayName: "German Liechtenstein" },
{ languageTag: "de-LU", displayName: "German Luxembourg" },
{ languageTag: "de-CH", displayName: "German Switzerland" },
{ languageTag: "el-GR", displayName: "Greek Greece" },
{ languageTag: "gu-IN", displayName: "Gujarati India" },
{ languageTag: "he-IL", displayName: "Hebrew Israel" },
{ languageTag: "hi-IN", displayName: "Hindi India" },
{ languageTag: "hu-HU", displayName: "Hungarian Hungary" },
{ languageTag: "is-IS", displayName: "Icelandic Iceland" },
{ languageTag: "id-ID", displayName: "Indonesian Indonesia" },
{ languageTag: "it-IT", displayName: "Italian Italy" },
{ languageTag: "it-CH", displayName: "Italian Switzerland" },
{ languageTag: "ja-JP", displayName: "Japanese Japan" },
{ languageTag: "kn-IN", displayName: "Kannada India" },
{ languageTag: "kk-KZ", displayName: "Kazakh Kazakhstan" },
{ languageTag: "kok-IN", displayName: "Konkani India" },
{ languageTag: "ko-KR", displayName: "Korean Korea" },
{ languageTag: "ky-KZ", displayName: "Kyrgyz Kazakhstan" },
{ languageTag: "lv-LV", displayName: "Latvian Latvia" },
{ languageTag: "lt-LT", displayName: "Lithuanian Lithuania" },
{ languageTag: "mk-MK", displayName: "Macedonian (FYROM)" },
{ languageTag: "ms-BN", displayName: "Malay Brunei" },
{ languageTag: "ms-MY", displayName: "Malay Malaysia" },
{ languageTag: "mr-IN", displayName: "Marathi India" },
{ languageTag: "mn-MN", displayName: "Mongolian Mongolia" },
{ languageTag: "nb-NO", displayName: "Norwegian (Bokmål) Norway" },
{ languageTag: "nn-NO", displayName: "Norwegian (Nynorsk) Norway" },
{ languageTag: "pl-PL", displayName: "Polish Poland" },
{ languageTag: "pt-BR", displayName: "Portuguese Brazil" },
{ languageTag: "pt-PT", displayName: "Portuguese Portugal" },
{ languageTag: "pa-IN", displayName: "Punjabi India" },
{ languageTag: "ro-RO", displayName: "Romanian Romania" },
{ languageTag: "ru-RU", displayName: "Russian Russia" },
{ languageTag: "sa-IN", displayName: "Sanskrit India" },
{ languageTag: "Cy-sr-SP", displayName: "Serbian (Cyrillic) Serbia" },
{ languageTag: "Lt-sr-SP", displayName: "Serbian (Latin) Serbia" },
{ languageTag: "sk-SK", displayName: "Slovak Slovakia" },
{ languageTag: "sl-SI", displayName: "Slovenian Slovenia" },
{ languageTag: "es-AR", displayName: "Spanish Argentina" },
{ languageTag: "es-BO", displayName: "Spanish Bolivia" },
{ languageTag: "es-CL", displayName: "Spanish Chile" },
{ languageTag: "es-CO", displayName: "Spanish Colombia" },
{ languageTag: "es-CR", displayName: "Spanish Costa Rica" },
{ languageTag: "es-DO", displayName: "Spanish Dominican Republic" },
{ languageTag: "es-EC", displayName: "Spanish Ecuador" },
{ languageTag: "es-SV", displayName: "Spanish El Salvador" },
{ languageTag: "es-GT", displayName: "Spanish Guatemala" },
{ languageTag: "es-HN", displayName: "Spanish Honduras" },
{ languageTag: "es-MX", displayName: "Spanish Mexico" },
{ languageTag: "es-NI", displayName: "Spanish Nicaragua" },
{ languageTag: "es-PA", displayName: "Spanish Panama" },
{ languageTag: "es-PY", displayName: "Spanish Paraguay" },
{ languageTag: "es-PE", displayName: "Spanish Peru" },
{ languageTag: "es-PR", displayName: "Spanish Puerto Rico" },
{ languageTag: "es-ES", displayName: "Spanish Spain" },
{ languageTag: "es-UY", displayName: "Spanish Uruguay" },
{ languageTag: "es-VE", displayName: "Spanish Venezuela" },
{ languageTag: "sw-KE", displayName: "Swahili Kenya" },
{ languageTag: "sv-FI", displayName: "Swedish Finland" },
{ languageTag: "sv-SE", displayName: "Swedish Sweden" },
{ languageTag: "syr-SY", displayName: "Syriac Syria" },
{ languageTag: "ta-IN", displayName: "Tamil India" },
{ languageTag: "tt-RU", displayName: "Tatar Russia" },
{ languageTag: "te-IN", displayName: "Telugu India" },
{ languageTag: "th-TH", displayName: "Thai Thailand" },
{ languageTag: "tr-TR", displayName: "Turkish Turkey" },
{ languageTag: "uk-UA", displayName: "Ukrainian Ukraine" },
{ languageTag: "ur-PK", displayName: "Urdu Pakistan" },
{ languageTag: "Cy-uz-UZ", displayName: "Uzbek (Cyrillic) Uzbekistan" },
{ languageTag: "Lt-uz-UZ", displayName: "Uzbek (Latin) Uzbekistan" },
{ languageTag: "vi-VN", displayName: "Vietnamese Vietnam" },
];
export const DirectoryPropertyName = [
"UserPrincipalName",
"Fax2",
"StreetAddress",
"PostalCode",
"StateOrProvince",
"Alias",
"CustomAttribute1",
"CustomAttribute2",
"CustomAttribute3",
"CustomAttribute4",
"CustomAttribute5",
"CustomAttribute6",
"CustomAttribute7",
"CustomAttribute8",
"CustomAttribute9",
"CustomAttribute10",
"CustomAttribute11",
"CustomAttribute12",
"CustomAttribute13",
"CustomAttr§ibute14",
"CustomAttribute15",
];

View File

@ -0,0 +1,16 @@
$default-background: #f3f2f1;
$default-color: #252423;
$default-button-background: #6264a7;
$default-Button-color: #f3f2f1;
// dark theme
$dark-background: #2d2c2c;
$dark-color: #ffffff;
$dark-button-background: #6264a7;
$dark-button-color: #2d2c2c;
// contrast theme
$contrast-background: #000000;
$contrast-color: #ffffff;
$contrast-button-background: #b5c01c;
$contrast-Button-color: #000000;

View File

@ -0,0 +1,9 @@
export interface IAnnotation {
displayName: string;
localizations: ILocalization[];
}
export interface ILocalization {
languageTag: string;
displayName: string;
}

View File

@ -0,0 +1,6 @@
export interface IListItem {
key: string;
displayAttribute:string;
displayName: string;
localizations: string;
}

View File

@ -0,0 +1,59 @@
export interface IOrganizationReturnData {
'@odata.context': string;
value: IOrganization[];
}
export interface IOrganization {
id: string;
deletedDateTime?: any;
businessPhones: string[];
city: string;
country?: any;
countryLetterCode: string;
createdDateTime: string;
displayName: string;
marketingNotificationEmails: any[];
isMultipleDataLocationsForServicesEnabled?: any;
onPremisesLastSyncDateTime: string;
onPremisesLastPasswordSyncDateTime: string;
onPremisesSyncEnabled: boolean;
postalCode: string;
preferredLanguage: string;
securityComplianceNotificationMails: any[];
securityComplianceNotificationPhones: any[];
state?: any;
street: string;
technicalNotificationMails: string[];
tenantType: string;
privacyProfile?: any;
assignedPlans: AssignedPlan[];
provisionedPlans: ProvisionedPlan[];
directorySizeQuota: DirectorySizeQuota;
verifiedDomains: VerifiedDomain[];
}
interface VerifiedDomain {
capabilities: string;
isDefault: boolean;
isInitial: boolean;
name: string;
type: string;
}
interface DirectorySizeQuota {
used: number;
total: number;
}
interface ProvisionedPlan {
capabilityStatus: string;
provisioningStatus: string;
service: string;
}
interface AssignedPlan {
assignedDateTime: string;
capabilityStatus: string;
service: string;
servicePlanId: string;
}

View File

@ -0,0 +1,5 @@
import { IProfileCardProperty } from "./IProfileCardProperty";
export interface IProfileCardPropertiesResults {
'@odata.context': string;
value: IProfileCardProperty[];
}

View File

@ -0,0 +1,6 @@
import { IAnnotation} from "./IAnnotations";
export interface IProfileCardProperty {
directoryPropertyName : string;
annotations: IAnnotation[];
}

View File

@ -0,0 +1,5 @@
import { IAnnotation} from "./IAnnotations";
export interface IUpdateProfileCardProperty {
annotations: IAnnotation[];
}

View File

@ -0,0 +1,5 @@
import { ILocalization } from "./IAnnotations";
export interface ILocalizationExtended extends ILocalization {
languageDescription:string;
}

View File

@ -0,0 +1,555 @@
import { AppContext, IAppContextProps } from "../../Common/AppContextProps";
import { IProfileCardProperty } from "../../Entities/IProfileCardProperty";
import { IAnnotation, ILocalization } from "../../Entities/IAnnotations";
import { languages, DirectoryPropertyName } from "../../Common/constants";
import {
DefaultButton,
PrimaryButton,
} from "office-ui-fabric-react/lib/Button";
import { Panel, PanelType } from "office-ui-fabric-react/lib/Panel";
import { useConstCallback } from "@uifabric/react-hooks";
import React, { useContext, useEffect } from "react";
import { useProfileCardProperties } from "../../hooks/useProfileCardProperties";
import { IAddProfileCardPropertyProps } from "../AddProfileCardProperty/IAddProfileCardPropertyProps";
import { IAddProfileCardPropertyState } from "../AddProfileCardProperty/IAddProfileCardPropertyState";
import * as _ from "lodash";
import {
Stack,
ComboBox,
TextField,
Label,
Icon,
IComboBox,
CommandButton,
IIconProps,
initializeIcons,
IComboBoxOption,
mergeStyleSets,
IIconStyles,
ILabelStyles,
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
} from "office-ui-fabric-react";
import { ILocalizationExtended } from "../../Entities/IlocalizationExtended";
import strings from "ManageProfileCardPropertiesWebPartStrings";
// Component
// Add Profile Property Component
export const AddProfileCardProperty: React.FunctionComponent<IAddProfileCardPropertyProps> = (
props: IAddProfileCardPropertyProps
) => {
const applicationContext: IAppContextProps = useContext(AppContext); // Get React Context
const newIcon: IIconProps = { iconName: "Add" };
const { displayPanel, onDismiss } = props;
const { newProfileCardProperty } = useProfileCardProperties(); // Get method from hook
// Component Styles
const compomentStyles = mergeStyleSets({
addButtonContainer: {
width: 60,
display: "Flex",
paddingTop: 15,
paddingRight: 10,
alignContent: "center",
justifyContent: "center",
},
});
// Icon styles of labels
const iconlabelsStyles: IIconStyles = {
root: {
fontSize: 26,
color: applicationContext.themeVariant.palette.themePrimary,
},
};
// Label Styles
const labelStyles: ILabelStyles = {
root: { fontSize: 16 },
};
// Language Options
const _languagesOptions: IComboBoxOption[] = languages.map((language, i) => ({
key: language.languageTag,
text: `${language.displayName} (${language.languageTag})`,
}));
const [state, setState] = React.useState<IAddProfileCardPropertyState>({
isloading: false,
hasError: false,
errorMessage: "",
isSaving: false,
displayPanel: displayPanel,
profileCardProperties: {
directoryPropertyName: "",
annotations: [{ displayName: "", localizations: [] }],
},
disableAdd: true,
addLocalizationItem: {} as ILocalizationExtended,
languagesOptions: _languagesOptions,
directoryPropertyOptions: [],
});
// Run on Compoment did mount
useEffect(() => {
(() => {
const { listItems } = applicationContext;
const _directoryPropertyOptions: IComboBoxOption[] = [];
// Check if the Property already selected
for (const directoryProperty of DirectoryPropertyName) {
const directoryPropertyExists = _.findKey(listItems, {
key: directoryProperty,
});
if (directoryPropertyExists) continue; // Already Exist Property
_directoryPropertyOptions.push({
key: directoryProperty,
text: directoryProperty,
});
}
// update component State
setState({
...state,
directoryPropertyOptions: _directoryPropertyOptions,
profileCardProperties: {
annotations: [{ displayName: "", localizations: [] }],
directoryPropertyName: _directoryPropertyOptions[0].key.toString(),
},
});
})();
}, []);
// Button Styles
const buttonStyles = { root: { marginRight: 8 } };
// On dismiss Panel
const _dismissPanel = useConstCallback(() => {
setState({ ...state, displayPanel: false });
onDismiss(true);
});
// On Change Display Name
const _onChangeDisplayName = (
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
value: string
) => {
ev.preventDefault();
let { profileCardProperties } = state;
profileCardProperties.annotations[0].displayName = value;
setState({ ...state, profileCardProperties: profileCardProperties });
};
// On Direwctory Property Change
const _onDirectoryPropertyChange = (
ev: React.FormEvent<IComboBox>,
option?: IComboBoxOption
) => {
ev.preventDefault();
let { profileCardProperties } = state;
profileCardProperties.directoryPropertyName = option.text;
setState({ ...state, profileCardProperties: profileCardProperties });
};
// On Language Change
const _onLanguageChange = (
ev: React.FormEvent<IComboBox>,
option?: IComboBoxOption
) => {
ev.preventDefault();
console.log(option);
let { addLocalizationItem } = state;
addLocalizationItem = {
...addLocalizationItem,
languageTag: option.key.toString(),
languageDescription: option.text,
};
if (addLocalizationItem.displayName && addLocalizationItem.languageTag) {
setState({
...state,
disableAdd: false,
addLocalizationItem: addLocalizationItem,
});
} else {
setState({
...state,
disableAdd: true,
addLocalizationItem: addLocalizationItem,
});
}
};
// On Change LocalizationDisplayName
const _onChangeLocalizationisplayName = (
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
value: string
) => {
ev.preventDefault();
let { addLocalizationItem } = state;
addLocalizationItem = { ...addLocalizationItem, displayName: value };
if (addLocalizationItem.displayName && addLocalizationItem.languageTag) {
setState({
...state,
disableAdd: false,
addLocalizationItem: addLocalizationItem,
});
} else {
setState({
...state,
disableAdd: true,
addLocalizationItem: addLocalizationItem,
});
}
};
// onCLick Add Property
const _onAddProperty = async (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
const { msGraphClient, organizationId } = applicationContext;
const { profileCardProperties } = state;
const _localizationsExtended =
profileCardProperties.annotations[0].localizations;
setState({...state, isSaving: true, disableAdd: true});
let _profileCardProperty: IProfileCardProperty = {} as IProfileCardProperty;
let _localizations: ILocalization[] = [];
for (const _localizationExtended of _localizationsExtended) {
_localizations.push({
displayName: _localizationExtended.displayName,
languageTag: _localizationExtended.languageTag,
});
}
let _annotations: IAnnotation[] = [
{
displayName: profileCardProperties.annotations[0].displayName,
localizations: _localizations,
},
];
_profileCardProperty = {
directoryPropertyName: profileCardProperties.directoryPropertyName,
annotations: _annotations,
};
try {
const newProfileCardProperties = await newProfileCardProperty(
msGraphClient,
organizationId,
_profileCardProperty
);
onDismiss(true);
} catch (error) {
const _errorMessage:string = error.message ? error.message : strings.DefaultErrorMessageText;
setState({ ...state, hasError: true, errorMessage: _errorMessage , isSaving: false, disableAdd: false});
console.log(error);
}
};
//************************************************************************************************* */
// On Render Footer
//************************************************************************************************* */
const _onRenderFooterContent = (): JSX.Element => {
const { profileCardProperties, isSaving } = state;
let disableAddButton: boolean = true;
if (
profileCardProperties.directoryPropertyName &&
profileCardProperties.annotations[0].displayName
) {
disableAddButton = false;
}
return (
<div>
<PrimaryButton
disabled={disableAddButton}
onClick={_onAddProperty}
styles={buttonStyles}
>
{isSaving ? (
<Spinner size={SpinnerSize.xSmall}></Spinner>
) : (
strings.SaveButtonText
)}
</PrimaryButton>
<DefaultButton onClick={_dismissPanel}>
{strings.CancelButtonText}
</DefaultButton>
</div>
);
};
// On add new localization
const _onAddNewLocalization = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
// tslint:disable-next-line: no-shadowed-variable
const {
profileCardProperties,
addLocalizationItem,
// tslint:disable-next-line: no-shadowed-variable
languagesOptions,
} = state;
let _localizations = profileCardProperties.annotations[0].localizations;
_localizations.push(addLocalizationItem);
profileCardProperties.annotations[0].localizations = _localizations;
const { languageTag } = addLocalizationItem;
const newLaguageOptions: IComboBoxOption[] = _.filter(
languagesOptions,
(l) => {
return l.key !== languageTag;
}
);
// Update State
setState({
...state,
profileCardProperties: profileCardProperties,
addLocalizationItem: {
displayName: "",
languageTag: "",
languageDescription: "",
},
languagesOptions: newLaguageOptions,
});
};
// on Delete Localization
const _onDeleteLocalization = (ev: React.MouseEvent<HTMLButtonElement>) => {
// tslint:disable-next-line: no-shadowed-variable
let { languagesOptions, profileCardProperties } = state;
const _languageTag = ev.currentTarget.getAttribute("data-language-tag");
let _localizations: ILocalization[] =
profileCardProperties.annotations[0].localizations;
// get all localization without the deleted localization
const newlocalizations: ILocalization[] = _.filter(_localizations, (l) => {
return l.languageTag !== _languageTag;
});
// Add new localization without the deleted localization
profileCardProperties.annotations[0].localizations = newlocalizations;
// Get Deleted Localization
const deletedLocalization: ILocalization[] = _.filter(
_localizations,
(l) => {
return l.languageTag == _languageTag;
}
);
// Add deleted Localization to combo box of language
const _deletedLocalization: ILocalizationExtended = deletedLocalization[0] as ILocalizationExtended;
languagesOptions.push({
key: _deletedLocalization.languageTag,
text: `${_deletedLocalization.languageDescription}`,
});
// Re render component
setState({
...state,
profileCardProperties: profileCardProperties,
languagesOptions: _.sortBy(languagesOptions, ["text"]), // Sort language Options
});
};
//************************************************************************************************* */
// Test if there are Profile Card Properties
//************************************************************************************************* */
let { annotations, directoryPropertyName } = state.profileCardProperties;
let _displayName: string = "";
let localizations: ILocalization[] = [];
if (annotations) {
_displayName = annotations[0].displayName;
if (annotations[0].localizations) {
localizations = annotations[0].localizations;
}
}
/* if (!directoryPropertyName) {
directoryPropertyName = directoryPropertyOptions[0].key.toString();
} */
//************************************************************************************************* */
// Render Control
//************************************************************************************************* */
const {
isloading,
hasError,
errorMessage,
languagesOptions,
directoryPropertyOptions,
} = state;
if (isloading) {
return <Spinner size={SpinnerSize.medium} label={strings.LoadingText} />;
}
return (
<>
<Panel
isOpen={state.displayPanel}
onDismiss={_dismissPanel}
type={PanelType.custom}
customWidth={"600px"}
closeButtonAriaLabel="Close"
headerText={strings.AddPanelHeaderText}
onRenderFooterContent={_onRenderFooterContent}
isFooterAtBottom={true}
>
<Stack
style={{ marginTop: 30, marginBottom: 30 }}
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
>
{hasError && (
<MessageBar messageBarType={MessageBarType.error}>
{errorMessage}
</MessageBar>
)}
<Icon iconName="EditContact" styles={iconlabelsStyles}></Icon>
<Label styles={labelStyles}>{strings.PanelHeaderLabel}</Label>
</Stack>
<Stack tokens={{ childrenGap: 10 }}>
<ComboBox
required={true}
label="Directory Property Name"
allowFreeform={false}
autoComplete="on"
selectedKey={directoryPropertyName}
onChange={_onDirectoryPropertyChange}
options={directoryPropertyOptions}
/>
<TextField
required={true}
label="Display Name"
autoComplete="on"
name="displayName"
value={_displayName}
validateOnLoad={false}
validateOnFocusOut={true}
onGetErrorMessage={(value: string) => {
if (!value) {
return "Field is Required";
} else {
return;
}
}}
onChange={_onChangeDisplayName}
/>
<Stack
style={{ marginTop: 30 }}
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
>
<Icon iconName="World" styles={iconlabelsStyles}></Icon>
<Label styles={labelStyles}>Localization</Label>
</Stack>
<Stack
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
>
<ComboBox
required={true}
label="Language"
styles={{ root: { width: 300 } }}
dropdownWidth={300}
allowFreeform
autoComplete="on"
selectedKey={state.addLocalizationItem.languageTag}
options={languagesOptions}
onChange={_onLanguageChange}
/>
<TextField
required={true}
label="Display Name"
styles={{ field: { width: 190 } }}
value={state.addLocalizationItem.displayName}
onChange={_onChangeLocalizationisplayName}
/>
<div className={compomentStyles.addButtonContainer}>
<CommandButton
iconProps={newIcon}
title="Add"
ariaLabel="Add"
onClick={_onAddNewLocalization}
styles={{
icon: { fontSize: 26 },
}}
disabled={state.disableAdd}
/>
</div>
</Stack>
<div style={{ height: "100%", overflow: "auto" }}>
{localizations.map(
(localization: ILocalizationExtended, i: number) => {
return (
<>
<Stack
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
styles={{ root: { marginTop: 5 } }}
>
<TextField
disabled
styles={{
root: { width: 300 },
field: {
color:
applicationContext.themeVariant.semanticColors
.inputText,
},
}}
value={localization.languageDescription}
/>
<TextField
disabled
styles={{
field: {
color:
applicationContext.themeVariant.semanticColors
.inputText,
width: 190,
},
}}
value={localization.displayName}
/>
<CommandButton
iconProps={{ iconName: "Delete" }}
data-language-tag={localization.languageTag}
title="Delete"
ariaLabel="Delete"
styles={{
icon: { fontSize: 26 },
}}
onClick={_onDeleteLocalization}
/>
</Stack>
</>
);
}
)}
</div>
</Stack>
</Panel>
</>
);
};

View File

@ -0,0 +1,4 @@
export interface IAddProfileCardPropertyProps {
displayPanel: boolean;
onDismiss: (refresh:boolean) => void;
}

View File

@ -0,0 +1,17 @@
import { IProfileCardProperty } from "../../Entities/IProfileCardProperty";
import { ILocalization } from "../../Entities/IAnnotations";
import { ILocalizationExtended } from "../../Entities/IlocalizationExtended";
import { IComboBoxOption } from "office-ui-fabric-react";
export interface IAddProfileCardPropertyState {
isloading: boolean;
hasError:boolean;
errorMessage:string;
isSaving:boolean;
displayPanel: boolean;
profileCardProperties: IProfileCardProperty;
disableAdd:boolean;
addLocalizationItem: ILocalizationExtended;
languagesOptions: IComboBoxOption[];
directoryPropertyOptions: IComboBoxOption[];
}

View File

@ -0,0 +1,117 @@
import { AppContext, IAppContextProps } from "../../Common/AppContextProps";
import {
DefaultButton,
PrimaryButton,
} from "office-ui-fabric-react/lib/Button";
import { useConstCallback } from "@uifabric/react-hooks";
import React, { useContext, useEffect } from "react";
import { useProfileCardProperties } from "../../hooks/useProfileCardProperties";
import { IDeleteProfileCardPropertyProps } from "./IDeleteProfileCardPropertyProps";
import { IDeleteProfileCardPropertyState } from "./IDeleteProfileCardPropertyState";
import * as _ from "lodash";
import {
Label,
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
Dialog,
DialogType,
DialogFooter,
IDialogContentProps,
} from "office-ui-fabric-react";
import strings from "ManageProfileCardPropertiesWebPartStrings";
// Component
// Delete Profile Property Component
export const DeleteProfileCardProperty: React.FunctionComponent<IDeleteProfileCardPropertyProps> = (
props: IDeleteProfileCardPropertyProps
) => {
const applicationContext: IAppContextProps = useContext(AppContext); // Get React Context
const { displayPanel, onDismiss } = props;
const { deleteProfileCardProperty } = useProfileCardProperties(); // Get method from hook
const [state, setState] = React.useState<IDeleteProfileCardPropertyState>({
isloading: true,
hasError: false,
errorMessage: "",
isDeleting: false,
displayPanel: displayPanel,
disableDelete: false,
});
// On dismiss Panel
const dismissPanel = useConstCallback(() => {
setState({ ...state, displayPanel: false });
onDismiss(false);
});
// Delete Profile Card Property
const _onDelete = async (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
const { msGraphClient, organizationId } = applicationContext;
setState({ ...state, isDeleting: true, disableDelete: true });
try {
await deleteProfileCardProperty(
msGraphClient,
organizationId,
props.directoryPropertyName
);
// Return to list and refresh indicator true
onDismiss(true);
} catch (error) {
const _errorMessage: string = error.message
? error.message
: strings.DefaultErrorMessageText;
setState({
...state,
hasError: true,
errorMessage: _errorMessage,
isDeleting: false,
disableDelete: false,
});
console.log(error);
}
};
const dialogContentProps:IDialogContentProps = {
type: DialogType.normal,
title: "Delete Profile Card Property",
// subText: `Do you want delete the Directory Property ${props.directoryPropertyName} from Card Profile ?`,
};
//************************************************************************************************* */
// Render Control
//************************************************************************************************* */
return (
<>
<Dialog
hidden={!state.displayPanel}
onDismiss={dismissPanel}
dialogContentProps={dialogContentProps}
modalProps={{ isBlocking: true }}
>
{state.hasError ? (
<MessageBar messageBarType={MessageBarType.error} isMultiline>
Error on delete <strong>{props.directoryPropertyName} </strong>
{state.errorMessage}
</MessageBar>
) : (
<Label>
Do you want delete the Directory Property<strong> {props.directoryPropertyName} </strong> from Profile Card ?
</Label>
)}
<DialogFooter>
<PrimaryButton disabled={state.disableDelete} onClick={_onDelete}>
{state.isDeleting ? (
<Spinner size={SpinnerSize.xSmall}></Spinner>
) : (
"Delete"
)}
</PrimaryButton>
<DefaultButton onClick={dismissPanel} text="Cancel" />
</DialogFooter>
</Dialog>
</>
);
};

View File

@ -0,0 +1,5 @@
export interface IDeleteProfileCardPropertyProps {
displayPanel: boolean;
onDismiss: (refresh:boolean) => void;
directoryPropertyName: string;
}

View File

@ -0,0 +1,13 @@
import { IProfileCardProperty } from "../../Entities/IProfileCardProperty";
import { ILocalization } from "../../Entities/IAnnotations";
import { ILocalizationExtended } from "../../Entities/IlocalizationExtended";
import { IComboBoxOption } from "office-ui-fabric-react";
export interface IDeleteProfileCardPropertyState {
isloading: boolean;
hasError:boolean;
errorMessage:string;
isDeleting:boolean;
displayPanel: boolean;
disableDelete:boolean;
}

View File

@ -0,0 +1,593 @@
import { AppContext, IAppContextProps } from "../../Common/AppContextProps";
import { IProfileCardProperty } from "../../Entities/IProfileCardProperty";
import { IAnnotation, ILocalization } from "../../Entities/IAnnotations";
import { languages } from "../../Common/constants";
import {
DefaultButton,
PrimaryButton,
} from "office-ui-fabric-react/lib/Button";
import { Panel, PanelType } from "office-ui-fabric-react/lib/Panel";
import { useConstCallback } from "@uifabric/react-hooks";
import React, { useContext, useEffect } from "react";
import { useProfileCardProperties } from "../../hooks/useProfileCardProperties";
import { IEditProfileCardPropertyProps } from "../EditProfileCardProperty/IEditProfileCardPropertyProps";
import { IEditProfileCardPropertyState } from "../EditProfileCardProperty/IEditProfileCardPropertyState";
import * as _ from "lodash";
import {
Stack,
ComboBox,
TextField,
Label,
Icon,
IComboBox,
CommandButton,
IIconProps,
initializeIcons,
IComboBoxOption,
mergeStyleSets,
IIconStyles,
ILabelStyles,
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
} from "office-ui-fabric-react";
import { ILocalizationExtended } from "../../Entities/IlocalizationExtended";
import strings from "ManageProfileCardPropertiesWebPartStrings";
// Component
// Edit Profile Property Component
export const EditProfileCardProperty: React.FunctionComponent<IEditProfileCardPropertyProps> = (
props: IEditProfileCardPropertyProps
) => {
const applicationContext: IAppContextProps = useContext(AppContext); // Get React Context
const newIcon: IIconProps = { iconName: "Add" };
const { displayPanel, onDismiss } = props;
const {
getProfileCardProperty,
updateProfileCardProperty,
} = useProfileCardProperties(); // Get method from hook
// Component Styles
const compomentStyles = mergeStyleSets({
addButtonContainer: {
width: 60,
display: "Flex",
paddingTop: 15,
paddingRight: 10,
alignContent: "center",
justifyContent: "center",
},
});
// Icon styles of labels
const iconlabelsStyles: IIconStyles = {
root: {
fontSize: 26,
color: applicationContext.themeVariant.palette.themePrimary,
},
};
// Label Styles
const labelStyles: ILabelStyles = {
root: { fontSize: 16 },
};
// Language Options
let _languagesOptions: IComboBoxOption[] = languages.map((language, i) => ({
key: language.languageTag,
text: `${language.displayName} (${language.languageTag})`,
}));
// State
const [state, setState] = React.useState<IEditProfileCardPropertyState>({
isloading: true,
hasError: false,
errorMessage: "",
isSaving: false,
displayPanel: displayPanel,
profileCardProperties: {
directoryPropertyName: "",
annotations: [{ displayName: "", localizations: [] }],
},
disableAdd: true,
addLocalizationItem: {} as ILocalizationExtended,
languagesOptions: _languagesOptions,
});
// Run on Compoment did mount
useEffect(() => {
(async () => {
const { msGraphClient, organizationId } = applicationContext;
// Get Profile Property
let _profileCardProperty: IProfileCardProperty = await getProfileCardProperty(
msGraphClient,
organizationId,
props.directoryPropertyName
);
const _localizations: ILocalization[] =
_profileCardProperty.annotations[0].localizations;
let _newLaguageOptions: IComboBoxOption[] = _languagesOptions;
let _newLocalizations: ILocalizationExtended[] = [];
// add to combobox language that already exists (Localization for Property)
// the localization array must be fullfil from vaklues of localiz<ation to add the language description , we only get from API the
// Langua tag
for (const _localization of _localizations) {
_newLaguageOptions = _.filter(_newLaguageOptions, (l) => {
return l.key !== _localization.languageTag;
});
const _selectedLanguage = _.filter(_languagesOptions, (l) => {
return l.key == _localization.languageTag;
});
_newLocalizations.push({
displayName: _localization.displayName,
languageDescription: _selectedLanguage[0].text,
languageTag: _localization.languageTag,
});
}
// set bnew Language Option and Localization with language description
_languagesOptions = _newLaguageOptions;
_profileCardProperty.annotations[0].localizations = _newLocalizations;
// update component State
setState({
...state,
isloading: false,
languagesOptions: _languagesOptions,
profileCardProperties: _profileCardProperty,
});
})();
}, []);
// Button Styles
const buttonStyles = { root: { marginRight: 8 } };
// On dismiss Panel
const dismissPanel = useConstCallback(() => {
setState({ ...state, displayPanel: false });
onDismiss(false);
});
// On Change Display Name
const _onChangeDisplayName = (
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
value: string
) => {
ev.preventDefault();
let { profileCardProperties } = state;
profileCardProperties.annotations[0].displayName = value;
setState({ ...state, profileCardProperties: profileCardProperties });
};
// On Language Change
const _onLanguageChange = (
ev: React.FormEvent<IComboBox>,
option?: IComboBoxOption
) => {
ev.preventDefault();
let { addLocalizationItem } = state;
addLocalizationItem = {
...addLocalizationItem,
languageTag: option.key.toString(),
languageDescription: option.text,
};
if (addLocalizationItem.displayName && addLocalizationItem.languageTag) {
setState({
...state,
disableAdd: false,
addLocalizationItem: addLocalizationItem,
});
} else {
setState({
...state,
disableAdd: true,
addLocalizationItem: addLocalizationItem,
});
}
};
// On Change LocalizationDisplayName
const _onChangeLocalizationisplayName = (
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
value: string
) => {
ev.preventDefault();
let { addLocalizationItem } = state;
addLocalizationItem = { ...addLocalizationItem, displayName: value };
if (addLocalizationItem.displayName && addLocalizationItem.languageTag) {
setState({
...state,
disableAdd: false,
addLocalizationItem: addLocalizationItem,
});
} else {
setState({
...state,
disableAdd: true,
addLocalizationItem: addLocalizationItem,
});
}
};
// onCLick Add Property
const _onUpdateProperty = async (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
const { msGraphClient, organizationId } = applicationContext;
const { profileCardProperties } = state;
const _localizationsExtended =
profileCardProperties.annotations[0].localizations;
setState({...state, isSaving: true, disableAdd: true});
let _profileCardProperty: IProfileCardProperty = {} as IProfileCardProperty;
let _localizations: ILocalization[] = [];
for (const _localizationExtended of _localizationsExtended) {
_localizations.push({
displayName: _localizationExtended.displayName,
languageTag: _localizationExtended.languageTag,
});
}
let _annotations: IAnnotation[] = [
{
displayName: profileCardProperties.annotations[0].displayName,
localizations: _localizations,
},
];
_profileCardProperty = {
directoryPropertyName: profileCardProperties.directoryPropertyName,
annotations: _annotations,
};
try {
const updatedProfileCardProperties = await updateProfileCardProperty(
msGraphClient,
organizationId,
_profileCardProperty
);
// Return to list and refresh indicator true
onDismiss(true);
} catch (error) {
const _errorMessage:string = error.message ? error.message : strings.DefaultErrorMessageText;
setState({ ...state, hasError: true, errorMessage: _errorMessage , isSaving: false, disableAdd: false});
console.log(error);
}
};
//************************************************************************************************* */
// On Render Footer
//************************************************************************************************* */
const _onRenderFooterContent = (): JSX.Element => {
const { profileCardProperties, isSaving } = state;
let disableAddButton: boolean = true;
if (
profileCardProperties.directoryPropertyName &&
profileCardProperties.annotations[0].displayName
) {
disableAddButton = false;
}
// Render Component
return (
<div>
<PrimaryButton
disabled={disableAddButton}
onClick={_onUpdateProperty}
styles={buttonStyles}
>
{isSaving ? (
<Spinner size={SpinnerSize.xSmall}></Spinner>
) : (
strings.SaveButtonText
)}
</PrimaryButton>
<DefaultButton onClick={dismissPanel}>
{strings.CancelButtonText}
</DefaultButton>
</div>
);
};
// On add new localization
const _onAddNewLocalization = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
// tslint:disable-next-line: no-shadowed-variable
const {
profileCardProperties,
addLocalizationItem,
// tslint:disable-next-line: no-shadowed-variable
languagesOptions,
} = state;
let _localizations = profileCardProperties.annotations[0].localizations;
_localizations.push(addLocalizationItem);
profileCardProperties.annotations[0].localizations = _localizations;
const { languageTag } = addLocalizationItem;
const newLaguageOptions: IComboBoxOption[] = _.filter(
languagesOptions,
(l) => {
return l.key !== languageTag;
}
);
// Update State
setState({
...state,
profileCardProperties: profileCardProperties,
addLocalizationItem: {
displayName: "",
languageTag: "",
languageDescription: "",
},
languagesOptions: newLaguageOptions,
});
};
// on Delete Localization
const _onDeleteLocalization = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
// tslint:disable-next-line: no-shadowed-variable
let { languagesOptions, profileCardProperties } = state;
const _languageTag = ev.currentTarget.getAttribute("data-language-tag");
let _localizations: ILocalization[] =
profileCardProperties.annotations[0].localizations;
// get all localization without the deleted localization
const newlocalizations: ILocalization[] = _.filter(_localizations, (l) => {
return l.languageTag !== _languageTag;
});
// Add new localization without the deleted localization
profileCardProperties.annotations[0].localizations = newlocalizations;
// Get Deleted Localization
const deletedLocalization: ILocalization[] = _.filter(
_localizations,
(l) => {
return l.languageTag == _languageTag;
}
);
// Add deleted Localization to combo box of language
const _deletedLocalization: ILocalizationExtended = deletedLocalization[0] as ILocalizationExtended;
languagesOptions.push({
key: _deletedLocalization.languageTag,
text: `${_deletedLocalization.languageDescription}`,
});
// Re render component
setState({
...state,
profileCardProperties: profileCardProperties,
languagesOptions: _.sortBy(languagesOptions, ["text"]), // Sort language Options
});
};
// OnChange Localization Display Name
const _onChangeLocalizationDisplayName = (
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue: string
) => {
ev.preventDefault();
let { profileCardProperties } = state;
let { localizations } = profileCardProperties.annotations[0];
const _languageTagIndex: HTMLElement = ev.target as HTMLElement;
const _index: number = Number(
_languageTagIndex.getAttribute("data-language-tag-index")
);
localizations[_index] = { ...localizations[_index], displayName: newValue };
profileCardProperties.annotations[0].localizations = localizations;
setState({ ...state, profileCardProperties: profileCardProperties });
console.log(state);
};
//************************************************************************************************* */
// Test if there are Profile Card Properties
//************************************************************************************************* */
let { annotations, directoryPropertyName } = state.profileCardProperties;
let _displayName: string = "";
let localizations: ILocalization[] = [];
if (annotations) {
_displayName = annotations[0].displayName;
if (annotations[0].localizations) {
localizations = annotations[0].localizations;
}
}
//************************************************************************************************* */
// Render Control
//************************************************************************************************* */
const { isloading, hasError, errorMessage, languagesOptions } = state;
// Is loading
if (isloading) {
return <Spinner size={SpinnerSize.medium} label={strings.LoadingText} />;
}
// Render
return (
<>
<Panel
isOpen={state.displayPanel}
onDismiss={dismissPanel}
type={PanelType.custom}
customWidth={"600px"}
closeButtonAriaLabel="Close"
headerText={strings.EditPanelHeaderText}
onRenderFooterContent={_onRenderFooterContent}
isFooterAtBottom={true}
>
{hasError && (
<Stack
style={{ marginTop: 30, marginBottom: 30 }}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
>
<MessageBar messageBarType={MessageBarType.error}>
{errorMessage}
</MessageBar>
</Stack>
)}
<Stack
style={{ marginTop: 30, marginBottom: 30 }}
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
>
<Icon iconName="EditContact" styles={iconlabelsStyles}></Icon>
<Label styles={labelStyles}>{strings.PanelHeaderLabel}</Label>
</Stack>
<Stack tokens={{ childrenGap: 10 }}>
<TextField
label="Directory Property Name"
defaultValue={directoryPropertyName}
readOnly={true}
styles={{
field: {
backgroundColor:
applicationContext.themeVariant.semanticColors
.disabledBackground,
},
}}
/>
<TextField
required={true}
label="Display Name"
autoComplete="on"
name="displayName"
value={_displayName}
validateOnLoad={false}
validateOnFocusOut={true}
onGetErrorMessage={(value: string) => {
if (!value) {
return "Field is Required";
} else {
return;
}
}}
onChange={_onChangeDisplayName}
/>
<Stack
style={{ marginTop: 30 }}
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
>
<Icon iconName="World" styles={iconlabelsStyles}></Icon>
<Label styles={labelStyles}>Localization</Label>
</Stack>
<Stack
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
>
<ComboBox
required={true}
label="Language"
styles={{ root: { width: 300 } }}
dropdownWidth={300}
allowFreeform
autoComplete="on"
selectedKey={state.addLocalizationItem.languageTag}
options={languagesOptions}
onChange={_onLanguageChange}
/>
<TextField
required={true}
label="Display Name"
styles={{ field: { width: 190 } }}
value={state.addLocalizationItem.displayName}
onChange={_onChangeLocalizationisplayName}
/>
<div className={compomentStyles.addButtonContainer}>
<CommandButton
iconProps={newIcon}
title="Add"
ariaLabel="Add"
onClick={_onAddNewLocalization}
styles={{
icon: { fontSize: 26 },
}}
disabled={state.disableAdd}
/>
</div>
</Stack>
<div style={{ height: "100%", overflow: "auto" }}>
{localizations.map(
(localization: ILocalizationExtended, index: number) => {
return (
<>
<Stack
horizontal={true}
tokens={{ childrenGap: 10 }}
horizontalAlign="start"
verticalAlign="center"
styles={{ root: { marginTop: 5 } }}
>
<TextField
disabled
styles={{
root: { width: 300 },
field: {
color:
applicationContext.themeVariant.semanticColors
.inputText,
},
}}
value={localization.languageDescription}
/>
<TextField
styles={{
field: {
color:
applicationContext.themeVariant.semanticColors
.inputText,
width: 190,
},
}}
data-language-tag-index={index}
value={localization.displayName}
onChange={_onChangeLocalizationDisplayName}
/>
<CommandButton
iconProps={{ iconName: "Delete" }}
data-language-tag={localization.languageTag}
title="Delete"
ariaLabel="Delete"
styles={{
icon: { fontSize: 26 },
}}
onClick={_onDeleteLocalization}
/>
</Stack>
</>
);
}
)}
</div>
</Stack>
</Panel>
</>
);
};

View File

@ -0,0 +1,5 @@
export interface IEditProfileCardPropertyProps {
displayPanel: boolean;
onDismiss: (refresh:boolean) => void;
directoryPropertyName: string;
}

View File

@ -0,0 +1,16 @@
import { IProfileCardProperty } from "../../Entities/IProfileCardProperty";
import { ILocalization } from "../../Entities/IAnnotations";
import { ILocalizationExtended } from "../../Entities/IlocalizationExtended";
import { IComboBoxOption } from "office-ui-fabric-react";
export interface IEditProfileCardPropertyState {
isloading: boolean;
hasError:boolean;
errorMessage:string;
isSaving:boolean;
displayPanel: boolean;
profileCardProperties: IProfileCardProperty;
disableAdd:boolean;
addLocalizationItem: ILocalizationExtended;
languagesOptions: IComboBoxOption[];
}

View File

@ -0,0 +1,5 @@
export interface IListCommandBarProps {
selectedItem :any;
onActionSelected: (action:string) => void;
onSearch: (searchCondition: string) => void;
}

View File

@ -0,0 +1,3 @@
export interface IListCommandBarState {
}

View File

@ -0,0 +1,115 @@
import * as React from "react";
import {
CommandBar,
ICommandBarItemProps,
} from "office-ui-fabric-react/lib/CommandBar";
import { IButtonProps } from "office-ui-fabric-react/lib/Button";
import { IListCommandBarProps } from "./IListCommandBarProps";
import { AppContext } from "../../Common/AppContextProps";
import { useState, useEffect } from "react";
import { SearchBox, ISearchBoxStyles, Label } from "office-ui-fabric-react";
const searchtyles: ISearchBoxStyles = {
root: { width: 320, marginRight: 15, marginTop: 5, marginBottom: 5 },
};
export const ListCommandBar: React.FunctionComponent<IListCommandBarProps> = (
props: IListCommandBarProps
) => {
const _applicationContext = React.useContext(AppContext);
const {} = _applicationContext;
const { selectedItem } = props;
let _disableNew: boolean = false;
let _disableEdit: boolean = true;
let _disableDelete: boolean = true;
let _disableView: boolean = true;
if (selectedItem) {
_disableEdit = false;
_disableDelete = false;
_disableView = false;
}
//
useEffect(() => {});
// On clear Search
const _onClear = () => {
let _searchCondition: string = "";
props.onSearch(_searchCondition);
};
const _onSearch = (value: string) => {
props.onSearch(value);
};
// CommandBar Options
const _items: ICommandBarItemProps[] = [
{
key: "newItem",
text: "New",
cacheKey: "myCacheKey", // changing this key will invalidate this item's cache
iconProps: { iconName: "Add" },
disabled: _disableNew,
onClick: () => props.onActionSelected("New"),
},
{
key: "edit",
text: "Edit",
iconProps: { iconName: "Edit" },
disabled: _disableEdit,
onClick: () => props.onActionSelected("Edit"),
},
/* {
key: "view",
text: "View",
iconProps: { iconName: "View" },
disabled: _disableView,
onClick: () => props.onActionSelected("View"),
}, */
{
key: "delete",
text: "Delete",
disabled: _disableDelete,
iconProps: { iconName: "Delete" },
onClick: () => props.onActionSelected("Delete"),
},
];
// FarItems
const _farItems: ICommandBarItemProps[] = [
{
key: "search",
text: "",
onRender: () => {
return (
<SearchBox
styles={searchtyles}
onSearch={_onSearch}
onClear={_onClear}
underlined={true}
placeholder="Search Properties"
/>
);
},
},
{
key: "refresh",
text: "refresh",
// This needs an ariaLabel since it's icon-only
ariaLabel: "refresh list",
iconOnly: true,
iconProps: { iconName: "Refresh" },
onClick: () => props.onActionSelected("Refresh"),
},
];
return (
<div>
<CommandBar items={_items} farItems={_farItems} />
</div>
);
};

View File

@ -0,0 +1,13 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import {
IReadonlyTheme
} from "@microsoft/sp-component-base";
import { DisplayMode } from "@microsoft/sp-core-library";
export interface IManageProfileCardPropertiesProps {
title: string;
webpartContext: WebPartContext;
themeVariant: IReadonlyTheme;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
}

View File

@ -0,0 +1,14 @@
import { IColumn } from "office-ui-fabric-react";
import { IListItem } from "../../Entities/IListItem";
export interface IManageProfileCardPropertiesState {
isLoading: boolean;
hasError:boolean;
errorMessage: string;
listItems: IListItem[];
listFields: IColumn[];
selectedItem: IListItem;
displayNewPanel: boolean;
displayEditPanel:boolean;
displayDeletePanel: boolean;
}

View File

@ -0,0 +1,74 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.manageProfileCardProperties {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,498 @@
import * as React from "react";
import styles from "./ManageProfileCardProperties.module.scss";
import { IManageProfileCardPropertiesProps } from "./IManageProfileCardPropertiesProps";
import { escape } from "@microsoft/sp-lodash-subset";
import { IManageProfileCardPropertiesState } from "./IManageProfileCardPropertiesState";
import { IListItem } from "../../Entities/IListItem";
import { MSGraphClient } from "@microsoft/sp-http";
import { ListCommandBar } from "../ListCommandBar/ListCommandBar";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import {
loadTheme,
mergeStyleSets,
MessageBarType,
FontIcon,
Toggle,
CustomizerContext,
Customizer,
MessageBar,
Spinner,
SpinnerSize,
} from "office-ui-fabric-react";
import {
DetailsList,
DetailsListLayoutMode,
Selection,
SelectionMode,
IColumn,
} from "office-ui-fabric-react/lib/DetailsList";
import { useProfileCardProperties } from "../../hooks/useProfileCardProperties";
import { IProfileCardProperty } from "../../Entities/IProfileCardProperty";
import { AppContext, IAppContextProps } from "../../Common/AppContextProps";
import { AddProfileCardProperty } from "../AddProfileCardProperty/AddProfileCardProperty";
import { EditProfileCardProperty } from "../EditProfileCardProperty/EditProfileCardProperty";
import { DeleteProfileCardProperty } from "../DeleteProfileCardProperty/DeleteProfileCardProperty";
import strings from "ManageProfileCardPropertiesWebPartStrings";
// Style Component
const classNames = mergeStyleSets({
commandBar: {
marginTop: 15,
marginBottom: 15,
},
title: {
fontSize: 22,
},
fileIconHeaderIcon: {
padding: 0,
fontSize: "16px",
},
fileIconCell: {
textAlign: "center",
selectors: {
"&:before": {
content: ".",
display: "inline-block",
verticalAlign: "middle",
height: "100%",
width: "0px",
visibility: "hidden",
},
},
},
fileIconImg: {
verticalAlign: "middle",
maxHeight: "16px",
maxWidth: "16px",
height: "16px",
width: "16px",
fontSize: "16px",
},
fileIconImgHeader: {
maxHeight: "16px",
maxWidth: "16px",
height: "16px",
width: "16px",
fontSize: "16px",
},
controlWrapper: {
display: "flex",
flexWrap: "wrap",
},
centerColumn: { display: "flex", alignItems: "flex-start", height: "100%" },
});
export default class ManageProfileCardProperties extends React.Component<
IManageProfileCardPropertiesProps,
IManageProfileCardPropertiesState
> {
private listFields: IColumn[] = [];
private organizationId: string = undefined;
private _selection: Selection;
private msGrapClient: MSGraphClient;
private applicationContext: IAppContextProps;
constructor(props: IManageProfileCardPropertiesProps) {
super(props);
this.listFields = [
{
name: "",
fieldName: "icon",
isResizable: false,
iconName: "TaskManager",
isIconOnly: true,
styles: { iconClassName: classNames.fileIconImgHeader },
minWidth: 20,
maxWidth: 20,
key: "icon",
onRender: (item: IListItem) => {
return (
<>
<FontIcon
iconName="TaskManager"
className={classNames.fileIconImg}
style={{ color: this.props.themeVariant.palette.themePrimary }}
/>
</>
);
},
},
{
key: "displayAttribute",
name: "Directory Property Name",
fieldName: "displayAttribute",
isResizable: true,
maxWidth: 210,
minWidth: 150,
isSorted: true,
isSortedDescending: false,
onColumnClick: this._onColumnClick,
},
{
name: "Display Name",
fieldName: "displayName",
isResizable: true,
maxWidth: 160,
minWidth: 50,
key: "displayName",
onColumnClick: this._onColumnClick,
},
{
name: "Nr Localizations",
fieldName: "localizations",
isResizable: true,
maxWidth: 150,
minWidth: 50,
key: "loc",
onColumnClick: this._onColumnClick,
},
];
this.state = {
isLoading: true,
hasError: false,
errorMessage: undefined,
listItems: [],
listFields: this.listFields,
selectedItem: undefined,
displayDeletePanel: false,
displayEditPanel: false,
displayNewPanel: false,
};
this._selection = new Selection({
onSelectionChanged: () => {
this.setState({
selectedItem: this._getSelectedItem(),
});
},
});
}
//
// Get Selected Item
//
private _getSelectedItem = (): IListItem => {
const selectionCount = this._selection.getSelectedCount();
switch (selectionCount) {
case 0:
return null;
case 1:
return this._selection.getSelection()[0] as IListItem;
default:
return null;
}
}
//
// Component did mount
//
public componentDidMount = async (): Promise<void> => {
try {
this.msGrapClient = await this.props.webpartContext.msGraphClientFactory.getClient();
// const _aadclient = await this.props.webpartContext.aadTokenProviderFactory.getTokenProvider();
this.setState({
isLoading: true,
});
const { checkUserIsGlobalAdmin } = await useProfileCardProperties();
const _isAdmin: Boolean = await checkUserIsGlobalAdmin(this.msGrapClient);
if (!_isAdmin) {
throw new Error(
"To Manage Profile Card Properties the user must be Tenant Admin"
);
}
this.organizationId = this.props.webpartContext.pageContext.aadInfo.tenantId;
const _listItems = await this._getProfileCardProperties();
this.applicationContext = {
...this.props,
listItems: _listItems,
msGraphClient: this.msGrapClient,
organizationId: this.organizationId,
};
this.setState({
listItems: _listItems,
isLoading: false,
});
} catch (error) {
this.setState({
hasError: true,
errorMessage: error.message,
isLoading: false,
});
console.log(error);
}
}
// Get Profile Properties
//
private _getProfileCardProperties = async (): Promise<IListItem[]> => {
const _listItems: IListItem[] = [];
const { getProfileCardProperties } = await useProfileCardProperties();
const _profileCardProperties: IProfileCardProperty[] = await getProfileCardProperties(
this.msGrapClient,
this.organizationId
);
if (_profileCardProperties.length > 0) {
for (const profileCardProperty of _profileCardProperties) {
_listItems.push({
key: profileCardProperty.directoryPropertyName,
displayAttribute: profileCardProperty.directoryPropertyName,
displayName: profileCardProperty.annotations[0].displayName,
localizations: profileCardProperty.annotations[0].localizations.length.toString(),
});
}
}
return _listItems;
}
// On Column Click
private _onColumnClick = (
ev: React.MouseEvent<HTMLElement>,
column: IColumn
): void => {
const { listFields, listItems } = this.state;
const newlistFields: IColumn[] = listFields.slice();
const currColumn: IColumn = newlistFields.filter(
(currCol) => column.key === currCol.key
)[0];
newlistFields.forEach((newCol: IColumn) => {
if (newCol === currColumn) {
currColumn.isSortedDescending = !currColumn.isSortedDescending;
currColumn.isSorted = true;
} else {
newCol.isSorted = false;
newCol.isSortedDescending = true;
}
});
const newItems = this._copyAndSort(
listItems,
currColumn.fieldName!,
currColumn.isSortedDescending
);
this.setState({
listFields: newlistFields,
listItems: newItems,
});
}
private _copyAndSort<T>(
items: T[],
columnKey: string,
isSortedDescending?: boolean
): T[] {
const key = columnKey as keyof T;
return items
.slice(0)
.sort((a: T, b: T) =>
(isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1
);
}
// Callback from ListCommand
private _onActionSelected = (option: string) => {
switch (option) {
case "New":
this.setState({
displayNewPanel: true,
});
break;
case "Edit":
this.setState({
displayEditPanel: true,
});
break;
case "Delete":
this.setState({
displayDeletePanel: true,
});
break;
case "Refresh":
this._onRefresh();
break;
default:
break;
}
}
// Reset sort Order columns to default
private _resetSortOrder = () => {
const { listFields } = this.state;
let _copyListFields: IColumn[] = [];
// Reset Sorted Fields
for (const listField of listFields) {
if (listField.fieldName == "displayAttribute") {
_copyListFields.push({
...listField,
isSorted: true,
isSortedDescending: false,
});
} else {
_copyListFields.push({
...listField,
isSorted: false,
isSortedDescending: false,
});
}
}
this.setState({ listFields: _copyListFields });
}
// On Refresh List
private _onRefresh = async () => {
this._resetSortOrder();
this._selection.setAllSelected(false);
this.setState({ isLoading: true, hasError: false, errorMessage: null });
const _listItems = await this._getProfileCardProperties();
// update Application context
this.applicationContext = {
...this.applicationContext,
listItems: _listItems,
};
// update State
this.setState({
listItems: _listItems,
selectedItem: undefined,
isLoading: false,
});
}
// On Dismiss Panel
private _onPanelDismiss = async (refresh: boolean) => {
if (refresh) {
this.setState({
displayEditPanel: false,
displayNewPanel: false,
displayDeletePanel: false,
});
// refresh List
await this._onRefresh();
} else {
this.setState({
displayEditPanel: false,
displayNewPanel: false,
displayDeletePanel: false,
});
}
}
// Search List
private _onSearch = async (value: string) => {
const { listItems } = this.applicationContext; // gloabal Items store in Application Context
let _filteredList: IListItem[] = [];
// blank value refresh the list
if (!value) {
_filteredList = listItems.slice();
} else {
// Filter
_filteredList = listItems.filter((item: IListItem) => {
if (
item.displayAttribute
.toLocaleLowerCase()
.indexOf(value.toLowerCase()) !== -1
) {
return item;
} else {
if (
item.displayName.toLowerCase().indexOf(value.toLowerCase()) !== -1
) {
return item;
} else {
return; // don't exists
}
}
});
}
this._resetSortOrder();
this._selection.setAllSelected(false);
this.setState({ listItems: _filteredList });
}
// Render Component
public render(): React.ReactElement<IManageProfileCardPropertiesProps> {
const {
hasError,
errorMessage,
listItems,
listFields,
selectedItem,
displayDeletePanel,
displayEditPanel,
displayNewPanel,
isLoading,
} = this.state;
// Has Error
if (hasError) {
return (
<MessageBar messageBarType={MessageBarType.error}>
{errorMessage}
</MessageBar>
);
}
// Is loading
if (isLoading) {
return (
<div style={{ paddingTop: 40 }}>
<Spinner size={SpinnerSize.medium} label={strings.LoadingText} />
</div>
);
}
// Render
return (
<AppContext.Provider value={this.applicationContext}>
<Customizer settings={{ theme: this.props.themeVariant }}>
<WebPartTitle
displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty}
/>
<ListCommandBar
onSearch={this._onSearch}
onActionSelected={this._onActionSelected}
selectedItem={selectedItem}
></ListCommandBar>
<DetailsList
items={listItems}
selection={this._selection}
columns={listFields}
selectionMode={SelectionMode.single}
setKey="prf"
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}
/>
{displayNewPanel && (
<AddProfileCardProperty
displayPanel={displayNewPanel}
onDismiss={this._onPanelDismiss}
></AddProfileCardProperty>
)}
{displayEditPanel && (
<EditProfileCardProperty
displayPanel={displayEditPanel}
directoryPropertyName={selectedItem.key}
onDismiss={this._onPanelDismiss}
></EditProfileCardProperty>
)}
{displayDeletePanel && (
<DeleteProfileCardProperty
displayPanel={displayDeletePanel}
directoryPropertyName={selectedItem.key}
onDismiss={this._onPanelDismiss}
></DeleteProfileCardProperty>
)}
</Customizer>
</AppContext.Provider>
);
}
}

View File

@ -0,0 +1,11 @@
import { IOrganization, IOrganizationReturnData } from "../Entities/IOrganization";
import { MSGraphClient } from "@microsoft/sp-http";
export const useGetOrganization = async (msGraphClient: MSGraphClient):Promise<IOrganization> => {
const _organization:IOrganizationReturnData = await msGraphClient
.api(`/organization`)
.version("beta")
.get();
return _organization.value[0];
};

View File

@ -0,0 +1,119 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { IProfileCardProperty } from "../Entities/IProfileCardProperty";
import { MSGraphClient, AadTokenProvider } from "@microsoft/sp-http";
import { useGetOrganization } from "./useGetOrganizationInfo";
import { IOrganization } from "../Entities/IOrganization";
import axios, { AxiosRequestConfig } from "axios";
import { IProfileCardPropertiesResults } from "../Entities/IProfileCardPropertiesResults";
import { IAnnotation } from "../Entities/IAnnotations";
import { IUpdateProfileCardProperty } from "../Entities/IUpdateProfileCardProperty";
const ADMIN_ROLETEMPLATE_ID = "62e90394-69f5-4237-9190-012177145e10";
export const useProfileCardProperties = () => {
// Get List of Properties
const getProfileCardProperties = async (
msGraphClient: MSGraphClient,
organizationId: string
):Promise<IProfileCardProperty[]> => {
const _profileProperties: IProfileCardPropertiesResults = await msGraphClient
.api(`/organization/${organizationId}/settings/profileCardProperties`)
.version("beta")
.orderby("directoryPropertyName")
.get();
/* const t = await msGraphClient.getToken('https://graph.microsoft.com');
const options : AxiosRequestConfig = {
method: 'get',
headers: { 'content-type': 'application/json', 'accept': 'application/json' , 'authorization': `bearer ${t}`},
url: `https://graph.microsoft.com/beta/organization/${organizationId}/settings/profileCardProperties`,
};
const _rs = await axios(options);
console.log(_rs); */
return _profileProperties.value;
};
// Add Property
const newProfileCardProperty = async (
msGraphClient: MSGraphClient,
organizationId: string,
profileCardProperties: IProfileCardProperty
):Promise<IProfileCardProperty> => {
const _profileProperty: IProfileCardProperty = await msGraphClient
.api(`/organization/${organizationId}/settings/profileCardProperties`)
.version("beta")
.post(profileCardProperties);
return _profileProperty;
};
// Update Profile Card Property
const updateProfileCardProperty = async (
msGraphClient: MSGraphClient,
organizationId: string,
profileCardProperties: IProfileCardProperty
):Promise<IProfileCardProperty> => {
const diretoryPropertyName:string = profileCardProperties.directoryPropertyName;
const _updateProfileCardProperty:IUpdateProfileCardProperty = { annotations : profileCardProperties.annotations};
const _profileProperty: IProfileCardProperty = await msGraphClient
.api(`/organization/${organizationId}/settings/profileCardProperties/${diretoryPropertyName}`)
.version("beta")
.patch(_updateProfileCardProperty);
return _profileProperty;
};
// get Profile Card Property
const getProfileCardProperty = async (
msGraphClient: MSGraphClient,
organizationId: string,
directoryPropertyName:string
):Promise<IProfileCardProperty> => {
const _profileProperty: IProfileCardProperty = await msGraphClient
.api(`/organization/${organizationId}/settings/profileCardProperties/${directoryPropertyName}`)
.version("beta")
.get();
return _profileProperty;
};
// Delete Profile Card Property
const deleteProfileCardProperty = async (
msGraphClient: MSGraphClient,
organizationId: string,
directoryPropertyName:string
) => {
const _profileProperty: IProfileCardProperty = await msGraphClient
.api(`/organization/${organizationId}/settings/profileCardProperties/${directoryPropertyName}`)
.version("beta")
.delete();
};
// check if user is Tenant Admin
const checkUserIsGlobalAdmin = async ( msGraphClient: MSGraphClient):Promise<boolean> => {
const myDirRolesAndGroupsResults = await msGraphClient
.api(`/me/memberof`)
.version("beta")
.get();
const myDirRolesAndGroups = myDirRolesAndGroupsResults ? myDirRolesAndGroupsResults.value : [];
for (const myDirRolesAndGroup of myDirRolesAndGroups) {
if (myDirRolesAndGroup.roleTemplateId && myDirRolesAndGroup.roleTemplateId === ADMIN_ROLETEMPLATE_ID) { // roleTemplateId for glabal Admin
return true;
}
}
return false;
};
// return
return {checkUserIsGlobalAdmin, getProfileCardProperties, newProfileCardProperty, updateProfileCardProperty, getProfileCardProperty, deleteProfileCardProperty };
};

View File

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

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "e62d11c6-4198-45b5-a04d-dee50230c5e3",
"alias": "ManageProfileCardPropertiesWebPart",
"componentType": "WebPart",
"supportsThemeVariants": true,
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart","SharePointFullPage","TeamsPersonalApp","TeamsTab"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Manage Profile Card Properties" },
"description": { "default": "Manage Profile Card Properties" },
"officeFabricIconFontName": "TaskManager",
"properties": {
"title": "Manage Profile Card Properties"
}
}]
}

View File

@ -0,0 +1,152 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, DisplayMode } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'ManageProfileCardPropertiesWebPartStrings';
import ManageProfileCardProperties from '../../components//ManageProfileCardProperties/ManageProfileCardProperties';
import { IManageProfileCardPropertiesProps } from '../../components/ManageProfileCardProperties/IManageProfileCardPropertiesProps';
import {
loadTheme,
mergeStyleSets,
MessageBarType,
FontIcon,
Toggle,
} from "office-ui-fabric-react";
const teamsDefaultTheme = require("../../common/TeamsDefaultTheme.json");
const teamsDarkTheme = require("../../common/TeamsDarkTheme.json");
const teamsContrastTheme = require("../../common/TeamsContrastTheme.json");
import {
ThemeProvider,
ThemeChangedEventArgs,
IReadonlyTheme,
} from "@microsoft/sp-component-base";
export interface IManageProfileCardPropertiesWebPartProps {
title: string;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
}
export default class ManageProfileCardPropertiesWebPart extends BaseClientSideWebPart <IManageProfileCardPropertiesWebPartProps> {
private _themeProvider: ThemeProvider;
private _themeVariant: IReadonlyTheme | undefined;
protected async onInit<T>(): Promise<T> {
/* this._tokenProvider = await this.context.aadTokenProviderFactory.getTokenProvider();
this._tokenToAzureVault = await this._tokenProvider.getToken('https://vault.azure.net');
const _value = await AzureDataStorageApisLibrary.getKeyVaultSecret('Teste',this._tokenToAzureVault);
console.log(_value); */
this._themeProvider = this.context.serviceScope.consume(
ThemeProvider.serviceKey
);
// If it exists, get the theme variant
this._themeVariant = this._themeProvider.tryGetTheme();
// Register a handler to be notified if the theme variant changes
this._themeProvider.themeChangedEvent.add(
this,
this._handleThemeChangedEvent
);
// id in teams
if (this.context.sdks.microsoftTeams) {
// in teams ?
const context = this.context.sdks.microsoftTeams!.context;
this._applyTheme(context.theme || "default");
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(
this._applyTheme
);
}
return Promise.resolve();
}
/**
* Update the current theme variant reference and re-render.
*
* @param args The new theme
*/
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
this._themeVariant = args.theme;
this.render();
}
// Apply btheme id in Teams
private _applyTheme = (theme: string): void => {
this.context.domElement.setAttribute("data-theme", theme);
document.body.setAttribute("data-theme", theme);
if (theme == "dark") {
loadTheme({
palette: teamsDarkTheme,
});
}
if (theme == "default") {
loadTheme({
palette: teamsDefaultTheme,
});
}
if (theme == "contrast") {
loadTheme({
palette: teamsContrastTheme,
});
}
}
public render(): void {
const element: React.ReactElement<IManageProfileCardPropertiesProps> = React.createElement(
ManageProfileCardProperties,
{
title: this.properties.title,
webpartContext: this.context,
themeVariant: this._themeVariant,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.title = value;
}
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('title', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,14 @@
define([], function() {
return {
DefaultErrorMessageText: "Error on update Card Profile, plase se Console Log to Details.",
CancelButtonText: "Cancel",
SaveButtonText: "Save",
LoadingText: "Loading",
PanelHeaderLabel: "Select Active Directory Property to be added to User Profile Card",
AddPanelHeaderText: "Add Profile Card Property",
EditPanelHeaderText: "Edit Profile Card Property",
"PropertyPaneDescription": "Manage Profile Card Properties",
"BasicGroupName": "Properties",
"DescriptionFieldLabel": "Web Part Title"
}
});

View File

@ -0,0 +1,17 @@
declare interface IManageProfileCardPropertiesWebPartStrings {
DefaultErrorMessageText: string;
CancelButtonText: string;
SaveButtonText: string;
LoadingText: string;
PanelHeaderLabel: string;
AddPanelHeaderText: string;
EditPanelHeaderText: string;
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'ManageProfileCardPropertiesWebPartStrings' {
const strings: IManageProfileCardPropertiesWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,64 @@
/**
* This script updates the package-solution version analogue to the
* the package.json file.
*/
if (process.env.npm_package_version === undefined) {
throw 'Package version cannot be evaluated';
}
// define path to package-solution file
const solution = './config/package-solution.json',
teams = './teams/manifest.json';
// require filesystem instance
const fs = require('fs');
// get next automated package version from process variable
const nextPkgVersion = process.env.npm_package_version;
// make sure next build version match
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
nextPkgVersion : nextPkgVersion.split('-')[0];
// Update version in SPFx package-solution if exists
if (fs.existsSync(solution)) {
// read package-solution file
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
// parse file as json
const solutionContents = JSON.parse(solutionFileContent);
// set property of version to next version
solutionContents.solution.version = nextVersion + '.0';
// save file
fs.writeFileSync(
solution,
// convert file back to proper json
JSON.stringify(solutionContents, null, 2),
'UTF-8');
}
// Update version in teams manifest if exists
if (fs.existsSync(teams)) {
// read package-solution file
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
// parse file as json
const teamsContent = JSON.parse(teamsManifestContent);
// set property of version to next version
teamsContent.version = nextVersion;
// save file
fs.writeFileSync(
teams,
// convert file back to proper json
JSON.stringify(teamsContent, null, 2),
'UTF-8');
}

View File

@ -0,0 +1,40 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": [
"src/**/*.ts", "src/components/AddProfileCardProperty/AddProfileCardProperty.tsx", "src/components/AddProfileCardProperty"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,29 @@
{
"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-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -3,6 +3,8 @@
"id": "ea67ede9-e4e7-411c-aee1-843dca97a4a8",
"alias": "OrganizationChartWebPart",
"componentType": "WebPart",
"supportsFullBleed": false,
"supportsThemeVariants": true,
// The "*" signifies that the version should be taken from the package.json
"version": "*",