Merge pull request #2406 from anoopt/master

Updated react-graph-cascading-managed-metadata to SPFx v1.14
This commit is contained in:
Hugo Bernier 2022-03-27 13:55:33 -04:00 committed by GitHub
commit dbd6015364
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 16931 additions and 9199 deletions

View File

@ -0,0 +1,39 @@
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
{
"name": "SPFx 1.14.0",
"image": "docker.io/m365pnp/spfx:1.14.0",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
4321,
35729
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
// Not needed for SPFx>= 1.12.1
// "5432": {
// "protocol": "https",
// "label": "Workbench",
// "onAutoForward": "silent"
// },
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -0,0 +1,33 @@
echo
echo -e "\e[1;94mInstalling Node dependencies\e[0m"
npm install
## commands to create dev certificate and copy it to the root folder of the project
echo
echo -e "\e[1;94mGenerating dev certificate\e[0m"
gulp trust-dev-cert
# Convert the generated PEM certificate to a CER certificate
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
# Copy the PEM ecrtificate for non-Windows hosts
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
## add *.cer to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.cer' ./.gitignore
then
echo "# .CER Certificates" >> .gitignore
echo "*.cer" >> .gitignore
fi
## add *.pem to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.pem' ./.gitignore
then
echo "# .PEM Certificates" >> .gitignore
echo "*.pem" >> .gitignore
fi
echo
echo -e "\e[1;92mReady!\e[0m"
echo -e "\n\e[1;94m**********\nOptional: if you plan on using gulp serve, don't forget to add the container certificate to your local machine. Please visit https://aka.ms/spfx-devcontainer for more information\n**********"

View File

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

@ -9,6 +9,7 @@ node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
@ -30,4 +31,7 @@ obj
# Styles Generated Code
*.scss.ts
*.scss.d.ts
*.scss.d.ts# .CER Certificates
*.cer
# .PEM Certificates
*.pem

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -2,7 +2,7 @@
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.9.1",
"version": "1.14.0",
"libraryName": "react-graph-cascading-managed-metadata",
"libraryId": "cdc626ca-e9a7-4d1d-bded-a574bc5e61d0",
"packageManager": "npm",

View File

@ -8,19 +8,14 @@ This web part shows how to use the Microsoft Graph APIs (beta) for Taxonomy to g
![Cascading managed metadata](./assets/cmmd.gif)
### Termstore
![Term store](./assets/termstore.png)
## Compatibility
![SPFx 1.11](https://img.shields.io/badge/SPFx-1.11.0-green.svg)
![Node.js v10](https://img.shields.io/badge/Node.js-v10-green.svg)
![SPFx 1.14](https://img.shields.io/badge/SPFx-1.14-green.svg)
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Teams Incompatible](https://img.shields.io/badge/Teams-Incompatible-lightgrey.svg)
![Local Workbench Incompatible](https://img.shields.io/badge/Local%20Workbench-Incompatible-yellow.svg "This solution requires access to data terms using Microsoft Graph API")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
@ -31,16 +26,18 @@ This web part shows how to use the Microsoft Graph APIs (beta) for Taxonomy to g
## Pre-requisites
* Set up the termset structure as shown in the image above.
* Set up the termset structure as shown in the image below - .
* To the termset, add a custom property called `UsedForShowingMaps` and set it's value to `true` as shown in the image below
![Term store properties](./assets/termsetproperties.png)
* For the cities, get the required latitude and longitude.
* Set the description of the city term as `latitude;longitude` (as highlighted for the term `London` in the image above).
* Add 2 custom properties for the city terms `latitude` and `longitude` (as highlighted for the term `London` in the image below).
![Term store](./assets/termstore.png)
## Solution
Solution|Author(s)
--------|---------
react-graph-cascading-managed-metadata| Anoop Tatti ([anoopt](https://github.com/anoopt), [@anooptells](https://twitter.com/anooptells))
react-graph-cascading-managed-metadata| Anoop Tatti ([anoopt](https://github.com/anoopt), ([https://linktr.ee/anoopt](https://linktr.ee/anoopt))
## Version history
@ -48,6 +45,7 @@ Version|Date|Comments
-------|----|--------
1.0.0|Aug 24, 2020|Initial release
1.0.1|Sep 03, 2020|Error handling and logging improvements
2.0.0|Mar 04, 2022|Updated to SPFx 1.14, used term `custom properties` to get co-ordinates (as Graph API provides that capability now), usage of `PropertyFieldGuid` and several other improvements
## Minimal Path to Awesome
@ -55,14 +53,17 @@ Version|Date|Comments
* in the command line run:
* `npm install`
* `gulp serve`
* Make sure you have completed the [pre-requisites](#Pre-requisites)
* Add the web part to the workbench page of a site
* Edit the web part and add the termset id in the properties
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit https://aka.ms/spfx-devcontainer for further instructions.
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
## Features
This sample illustrates the following concepts on top of the SharePoint Framework:
* Get data terms using Microsoft Graph API (beta).
* Get termset, terms and their custom properties using Microsoft Graph API (beta).
* React Hooks
* Using async / await for the async calls
* Caching the data in session storage
@ -72,12 +73,15 @@ This sample illustrates the following concepts on top of the SharePoint Framewor
### Enhancements
* Currently, this web part supports 2-level cascading. So there is scope to enhance this such that it supports more levels of cascading dynamically.
* Currently, this web part reads latitude and longitude from description of the city terms. If there is a way of getting these from the custom properties of the city terms, then that needs to be implemented.
## Video
[![Cascading managed metadata using Microsoft Graph and SharePoint Framework](./assets/video-thumbnail.jpg)](https://www.youtube.com/watch?v=lk47ijo_H6Y "Cascading managed metadata using Microsoft Graph and SharePoint Framework")
## Need to show more details?
An Adaptive Card Extension (ACE) which performs similar operations and provides more data like the local time of the office, weather data of the office location and address of the office location along with it's map can be found in the `Office locations` sample of [pnp/sp-dev-fx-aces repostory](https://github.com/pnp/sp-dev-fx-aces/tree/main/samples/ImageCard-OfficeLocations).
## Help
We do not support samples, but we this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
@ -90,11 +94,8 @@ For questions regarding this sample, [create a new question](https://github.com/
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-graph-cascading-managed-metadata&template=question.yml&sample=react-graph-cascading-managed-metadata&authors=@anoopt&title=react-graph-cascading-managed-metadata%20-%20).
## 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.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-graph-cascading-managed-metadata" />

View File

@ -9,7 +9,7 @@
"Shows how to use the Microsoft Graph APIs (beta) for Taxonomy to get the data."
],
"creationDateTime": "2020-09-02",
"updateDateTime": "2020-09-02",
"updateDateTime": "2022-03-02",
"products": [
"SharePoint"
],
@ -20,7 +20,7 @@
},
{
"key": "SPFX-VERSION",
"value": "1.11.0"
"value": "1.14.0"
}
],
"thumbnails": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -14,6 +14,7 @@
"externals": {},
"localizedResources": {
"CascadingManagedMetadataWebPartStrings": "lib/webparts/cascadingManagedMetadata/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/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

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

View File

@ -1,6 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-graph-cascading-managed-metadata",
"accessKey": "<!-- ACCESS KEY -->"

View File

@ -2,6 +2,21 @@
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-graph-cascading-managed-metadata-client-side-solution",
"developer": {
"name": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"websiteUrl": "",
"mpnId": "Undefined-1.14.0"
},
"metadata": {
"shortDescription": {
"default": "react-graph-cascading-managed-metadata description"
},
"longDescription": {
"default": "react-graph-cascading-managed-metadata description"
}
},
"id": "cdc626ca-e9a7-4d1d-bded-a574bc5e61d0",
"version": "1.0.0.0",
"includeClientSideAssets": true,
@ -11,4 +26,4 @@
"paths": {
"zippedPackage": "solution/react-graph-cascading-managed-metadata.sppkg"
}
}
}

View File

@ -2,9 +2,5 @@
"$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/"
}
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

View File

@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/s-KaiNet/spfx-fast-serve/master/schema/config.latest.schema.json",
"cli": {
"isLibraryComponent": false
},
"serve": {
"open": false
}
}

View File

@ -0,0 +1,24 @@
/*
* User webpack settings file. You can add your own settings here.
* Changes from this file will be merged into the base webpack configuration file.
* This file will not be overwritten by the subsequent spfx-fast-serve calls.
*/
// you can add your project related webpack configuration here, it will be merged using webpack-merge module
// i.e. plugins: [new webpack.Plugin()]
const webpackConfig = {
}
// for even more fine-grained control, you can apply custom webpack settings using below function
const transformConfig = function (initialWebpackConfig) {
// transform the initial webpack config here, i.e.
// initialWebpackConfig.plugins.push(new webpack.Plugin()); etc.
return initialWebpackConfig;
}
module.exports = {
webpackConfig,
transformConfig
}

View File

@ -1,36 +1,21 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
const argv = build.rig.getYargs().argv;
const useCustomServe = argv['custom-serve'];
const fs = require("fs");
const workbenchApi = require("@microsoft/sp-webpart-workbench/lib/api");
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
if (useCustomServe) {
build.tslintCmd.enabled = false;
const ensureWorkbenchSubtask = build.subTask('ensure-workbench-task', function (gulp, buildOptions, done) {
this.log('Creating workbench.html file...');
try {
workbenchApi.default["/workbench"]();
} catch (e) { }
result.set('serve', result.get('serve-deprecated'));
done();
});
return result;
};
build.rig.addPostBuildTask(build.task('ensure-workbench', ensureWorkbenchSubtask));
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
fs.writeFileSync("./temp/_webpack_config.json", JSON.stringify(generatedConfiguration, null, 2));
return generatedConfiguration;
}
});
}
/* fast-serve */
const { addFastServe } = require("spfx-fast-serve-helpers");
addFastServe(build);
/* end of fast-serve */
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -3,51 +3,34 @@
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"serve": "cross-env NODE_OPTIONS=--max_old_space_size=4096 gulp bundle --custom-serve && cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack-dev-server --mode development --config ./webpack.js --env.env=dev"
"serve": "gulp bundle --custom-serve --max_old_space_size=4096 && fast-serve"
},
"dependencies": {
"@microsoft/sp-core-library": "1.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"@pnp/spfx-controls-react": "^1.20.0-beta.52e4e9f",
"office-ui-fabric-react": "6.214.0",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
"@microsoft/sp-core-library": "1.14.0",
"@microsoft/sp-lodash-subset": "1.14.0",
"@microsoft/sp-office-ui-fabric-core": "1.14.0",
"@microsoft/sp-property-pane": "1.14.0",
"@microsoft/sp-webpart-base": "1.14.0",
"@pnp/spfx-controls-react": "3.5.0",
"@pnp/spfx-property-controls": "3.5.0",
"office-ui-fabric-react": "7.174.1",
"react": "16.13.1",
"react-dom": "16.13.1"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/sp-build-web": "1.11.0",
"@microsoft/sp-module-interfaces": "1.11.0",
"@microsoft/sp-tslint-rules": "1.11.0",
"@microsoft/sp-webpart-workbench": "1.11.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
"@microsoft/sp-build-web": "1.14.0",
"@microsoft/sp-module-interfaces": "1.14.0",
"@microsoft/sp-tslint-rules": "1.14.0",
"@types/react": "16.9.51",
"@types/react-dom": "16.9.8",
"@types/webpack-env": "1.13.1",
"ajv": "~5.2.2",
"cross-env": "7.0.2",
"css-loader": "3.4.2",
"css-modules-typescript-loader": "4.0.0",
"del": "5.1.0",
"fork-ts-checker-webpack-plugin": "4.1.0",
"gulp": "~3.9.1",
"node-sass": "4.13.1",
"sass-loader": "8.0.2",
"style-loader": "1.1.3",
"ts-loader": "6.2.1",
"webpack": "4.42.0",
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.10.3"
"gulp": "~4.0.2",
"spfx-fast-serve-helpers": "~1.14.0"
}
}

View File

@ -1,12 +1,9 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, DisplayMode, Guid } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { Version, DisplayMode } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { IPropertyPaneConfiguration } from "@microsoft/sp-property-pane";
import { PropertyFieldGuid } from '@pnp/spfx-property-controls/lib/PropertyFieldGuid';
import * as strings from 'CascadingManagedMetadataWebPartStrings';
import CascadingManagedMetadata from './components/CascadingManagedMetadata';
import { MSGraph } from './services/MSGraph';
@ -19,8 +16,12 @@ export default class CascadingManagedMetadataWebPart extends BaseClientSideWebPa
private _placeholder = null;
public async render(): Promise<void> {
protected async onInit(): Promise<void> {
await MSGraph.Init(this.context);
return super.onInit();
}
public async render(): Promise<void> {
let renderElement = null;
if (this.properties.termSetId) {
renderElement = React.createElement(
@ -68,18 +69,6 @@ export default class CascadingManagedMetadataWebPart extends BaseClientSideWebPa
return Version.parse('1.0');
}
private validateTermSetId(value: string): string {
if (value === null ||
value.trim().length === 0) {
return 'Provide a term set Id.';
}
if (!Guid.isValid(value.trim())) {
return 'Term set Id must be a valid GUID.';
}
return '';
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
@ -92,9 +81,11 @@ export default class CascadingManagedMetadataWebPart extends BaseClientSideWebPa
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('termSetId', {
PropertyFieldGuid('termSetId', {
key: 'termSetId',
label: strings.TermSetIdFieldLabel,
onGetErrorMessage: this.validateTermSetId.bind(this)
value: this.properties.termSetId,
errorMessage: "Term set Id must be a valid GUID"
})
]
}

View File

@ -1,20 +1,20 @@
import * as React from 'react';
import styles from './CascadingManagedMetadata.module.scss';
import { ICascadingManagedMetadataProps } from './ICascadingManagedMetadataProps';
import { MSGraph } from '../services/MSGraph';
import { ITerms } from '../../interfaces';
import { ICMMDDropdownOption, IProperty, ITerms } from '../../interfaces';
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { Map, ICoordinates } from "@pnp/spfx-controls-react/lib/Map";
import { MMDService } from '../services/MMDService';
import { find, isEmpty } from '@microsoft/sp-lodash-subset';
const CascadingManagedMetadata: React.SFC<ICascadingManagedMetadataProps> = (props) => {
const [countriesList, setCountriesList] = React.useState<IDropdownOption[]>([]);
const [citiesList, setCitiesList] = React.useState<IDropdownOption[]>([]);
const [selectedCityCoordinates, setSelectedCityCoordinates] = React.useState<string>(null);
const [citiesList, setCitiesList] = React.useState<ICMMDDropdownOption[]>([]);
const [selectedCityKey, setselectedCityKey] = React.useState<string>(null);
const [selectedCity, setSelectedCity] = React.useState<string>(null);
const [showMap, setShowMap] = React.useState<boolean>(false);
const [coordinates, setCoordinates] = React.useState<ICoordinates>({ latitude: null, longitude: null });
@ -26,22 +26,34 @@ const CascadingManagedMetadata: React.SFC<ICascadingManagedMetadataProps> = (pro
const LOG_SOURCE: string = "Cascading MMD -";
React.useEffect(() => {
_getCountries().then(countries => {
if (countries) {
const options: IDropdownOption[] = countries.value.map(c => ({ key: c.id, text: c.labels[0].name }));
setCountriesList(options);
} else {
setCountriesList([]);
clearData();
//* Check if the term set has a property called UsedForShowingMaps
const _checkIfTermsetIsUsedForShowingMaps = async (): Promise<boolean> => {
try {
const termsetData = await MSGraph.Call('get', `/termStore/sets/${props.termSetId}`, "beta", {}, ["properties"]);
const termsetProperties: IProperty[] = termsetData.properties;
console.debug("%s Retrieved termset properties. %o", LOG_SOURCE, termsetProperties);
if (isEmpty(termsetProperties)) {
return false;
}
});
}, [props.termSetId]); //* Run this also when the property termSetId changes
return find(termsetProperties, (p: IProperty) => p.key === "UsedForShowingMaps")?.value === "true";
}
catch (error) {
console.error("%s Error retrieving termset properties. Details - %o", LOG_SOURCE, error);
setMessageBarStatus({
type: MessageBarType.error,
message: <span>Error retrieving termset properties. Please contact admin.</span>,
show: true
});
return null;
}
};
//* Get the country terms i.e. level 1 children using Graph
const _getCountries = async (): Promise<ITerms> => {
try {
let countries: ITerms = await MSGraph.Get(`/termStore/sets/${props.termSetId}/children`, "beta");
let countries: ITerms = await MSGraph.Call('get', `/termStore/sets/${props.termSetId}/children`, "beta");
setMessageBarStatus(state => ({ ...state, show: false }));
console.debug("%s Retrieved countries. %o", LOG_SOURCE, countries);
return countries;
@ -58,51 +70,52 @@ const CascadingManagedMetadata: React.SFC<ICascadingManagedMetadataProps> = (pro
};
//* Get the city terms under a country i.e. level 2 children using Graph
const _onCountryChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
clearData();
let countryTermId: string = item.key.toString();
const _onCountryChange = async (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): Promise<void> => {
MMDService.GetTermsAsDropdownOptions(`/termStore/sets/${props.termSetId}/terms/${countryTermId}/children`, countryTermId, true)
.then(options => {
setCitiesList(options);
//setShowMap(false);
console.debug("%s Retrieved cities. %o", LOG_SOURCE, options);
setMessageBarStatus({
type: MessageBarType.warning,
message: options.length > 0 ?
<span>To see the map, please select a city. </span> :
<span> No city terms in the selected country.</span>,
show: true
});
})
.catch(error => {
console.error("%s Error retrieving cities. Details - %o", LOG_SOURCE, error);
setMessageBarStatus({
type: MessageBarType.error,
message: <span>Error in retrieving cities. Please contact admin.</span>,
show: true
});
setCitiesList([]);
try {
clearData();
let countryTermId: string = item.key.toString();
let cities: ICMMDDropdownOption[] = await MMDService
.GetTermsAsDropdownOptions(
`/termStore/sets/${props.termSetId}/terms/${countryTermId}/children`,
["id", "labels", "properties"],
countryTermId,
true);
setCitiesList(cities);
setMessageBarStatus({
type: MessageBarType.warning,
message: cities.length > 0 ?
<span>To see the map, please select a city. </span> :
<span> No city terms in the selected country.</span>,
show: true
});
console.debug("%s Retrieved cities. %o", LOG_SOURCE, cities);
}
catch (error) {
console.error("%s Error retrieving cities. Details - %o", LOG_SOURCE, error);
setMessageBarStatus({
type: MessageBarType.error,
message: <span>Error in retrieving cities. Please contact admin.</span>,
show: true
});
setCitiesList([]);
}
};
//* Extract co-ordinates from key of the dropdown option
//* The key will contain the description of the term
//* The description of the term will be of the format latitude;longitude
const _onCityChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setSelectedCity(item.text);
setSelectedCityCoordinates(item.key.toString());
const [lat, long] = item.key.toString().split(';');
const coordinates: ICoordinates = {
latitude: isNaN(Number(lat)) ? null : Number(lat),
longitude: isNaN(Number(long)) ? null : Number(long)
};
setCoordinates(coordinates);
console.debug("%s Retrieved coordinates. %o", LOG_SOURCE, coordinates);
const _onCityChange = (event: React.FormEvent<HTMLDivElement>, item: ICMMDDropdownOption): void => {
if (coordinates.latitude && coordinates.longitude) {
const { text, key, data } = item;
setSelectedCity(text);
setselectedCityKey(key.toString());
setCoordinates(data);
console.debug("%s Retrieved coordinates. %o", LOG_SOURCE, data);
if (data.latitude && data.longitude) {
setShowMap(true);
setMessageBarStatus(state => ({ ...state, show: false }));
} else {
@ -118,11 +131,59 @@ const CascadingManagedMetadata: React.SFC<ICascadingManagedMetadataProps> = (pro
//* Clear the data related to cities and maps
const clearData = () => {
setCoordinates({ latitude: null, longitude: null });
setSelectedCityCoordinates(null);
setselectedCityKey(null);
setSelectedCity(null);
setCitiesList([]);
setShowMap(false);
}
};
const _start = async (): Promise<void> => {
let isTermsetUsedForShowingMaps: boolean = await _checkIfTermsetIsUsedForShowingMaps();
if(isTermsetUsedForShowingMaps === null) {
return;
}
if (!isTermsetUsedForShowingMaps) {
setCountriesList([]);
clearData();
setMessageBarStatus({
type: MessageBarType.warning,
message: <span>The selected term set is not used for showing maps.</span>,
show: true
});
return;
}
let countries: ITerms = await _getCountries();
if(countries === null) {
return;
}
if (isEmpty(countries.value)) {
setCountriesList([]);
clearData();
setMessageBarStatus({
type: MessageBarType.warning,
message: <span>No country terms in the selected termset. Please contact admin.</span>,
show: true
});
return;
}
// Renamed to avoid shadowed variables
let localCountriesList: IDropdownOption[] = countries.value.map(c => ({ key: c.id, text: c.labels[0].name }));
setCountriesList(localCountriesList);
};
React.useEffect(() => {
_start();
}, [props.termSetId]); //* Run this also when the property termSetId changes
return (
<div>
@ -136,7 +197,7 @@ const CascadingManagedMetadata: React.SFC<ICascadingManagedMetadataProps> = (pro
<Dropdown
label="City"
selectedKey={selectedCityCoordinates}
selectedKey={selectedCityKey}
placeHolder="Select a city"
options={citiesList}
onChange={_onCityChange}

View File

@ -1,47 +1,44 @@
import { ITerms, IOption } from "../../interfaces";
import { ITerms, ICMMDDropdownOption } from "../../interfaces";
import { MSGraph } from "./MSGraph";
import { IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { find } from '@microsoft/sp-lodash-subset';
export class MMDService {
private static _sessionStorageKey: string = "CMMD_Options";
private static _logSource: string = "Cascading MMD Service -";
public static async GetTermsAsDropdownOptions(apiUrl: string, parent: string, tryFromCache: boolean): Promise<IDropdownOption[]> {
let options: IDropdownOption[] = [];
public static async GetTermsAsDropdownOptions(apiUrl: string, selectProperties: string[], parent: string, tryFromCache: boolean): Promise<ICMMDDropdownOption[]> {
if (tryFromCache) {
let optionsFromCache: IOption[] = this._fetchFromSessionStorge();
let optionsFromCache: ICMMDDropdownOption[] = this._fetchFromSessionStorge();
if (optionsFromCache.length) {
let requiredOptionsFromCache = optionsFromCache.filter(o => o.parent == parent);
if (requiredOptionsFromCache.length) {
options = requiredOptionsFromCache.map(r => ({ key: r.key, text: r.text }));
return options;
return requiredOptionsFromCache;
}
}
}
//Get data using Graph
return await this._getTermsAsDropdownOptionsUsingGraph(apiUrl, parent);
return await this._getTermsAsDropdownOptionsUsingGraph(apiUrl, selectProperties, parent);
}
private static async _getTermsAsDropdownOptionsUsingGraph(apiUrl: string, parent: string): Promise<IDropdownOption[]> {
private static async _getTermsAsDropdownOptionsUsingGraph(apiUrl: string, selectProperties: string[], parent: string): Promise<ICMMDDropdownOption[]> {
try {
let terms: ITerms = await MSGraph.Get(apiUrl, "beta");
let terms: ITerms = await MSGraph.Call("get", apiUrl, "beta", {}, selectProperties);
if (terms.value) {
//* Set key as description of the term
//* Description will be of the format latitude;longitude
//* This will be used to render maps
const options: IDropdownOption[] = terms.value.map(t => ({
key: t.descriptions[0] ? t.descriptions[0].description : t.id,
text: t.labels[0].name
}));
let optionsToStoreInCache: IOption[] = options.map(o => ({
key: o.key.toString(),
text: o.text,
const options: ICMMDDropdownOption[] = terms.value.map(t => ({
key: t.id,
text: t.labels[0].name,
data: {
latitude: Number(find(t.properties, p => p.key === "latitude")?.value) ?? null,
longitude: Number(find(t.properties, p => p.key === "longitude")?.value) ?? null,
},
parent
}));
let optionsFromCache: IOption[] = this._fetchFromSessionStorge();
optionsToStoreInCache = [...optionsFromCache, ...optionsToStoreInCache];
let optionsFromCache: ICMMDDropdownOption[] = this._fetchFromSessionStorge();
let optionsToStoreInCache: ICMMDDropdownOption[] = [...optionsFromCache, ...options];
window.sessionStorage.setItem(this._sessionStorageKey, JSON.stringify(optionsToStoreInCache));
console.debug("%s Data added in cache.", this._logSource);
return options;
@ -55,8 +52,8 @@ export class MMDService {
}
private static _fetchFromSessionStorge(): IOption[] {
let result: IOption[] = [];
private static _fetchFromSessionStorge(): ICMMDDropdownOption[] {
let result: ICMMDDropdownOption[] = [];
let stringResult: string = window.sessionStorage.getItem(this._sessionStorageKey);
if (stringResult) {
try {

View File

@ -1,3 +1,5 @@
//* Helper wrapper for calling Graph
//* based on https://gist.github.com/wobba/37416d3107b85675d896105554b3df28
//* Thank you Mikael Svenson
import { WebPartContext } from '@microsoft/sp-webpart-base';
@ -6,85 +8,38 @@ import { MSGraphClient } from '@microsoft/sp-http';
export class MSGraph {
private static _graphClient: MSGraphClient;
public static async Init(context: WebPartContext) {
this._graphClient = await context.msGraphClientFactory.getClient();
}
public static async Get(apiUrl: string, version: string = "v1.0", selectProperties?: string[], expandProperties?: string[], filter?: string): Promise<any> {
public static async Call(
method: "get" | "post" | "patch" | "delete",
apiUrl: string,
version: "v1.0" | "beta",
content?: any,
selectProperties?: string[],
expandProperties?: string[],
filter?: string,
count?: boolean
): Promise<any> {
var p = new Promise<string>(async (resolve, reject) => {
let query = this._graphClient.api(apiUrl).version(version);
if (selectProperties && selectProperties.length > 0) {
query = query.select(selectProperties);
}
if (filter && filter.length > 0) {
query = query.filter(filter);
}
if (expandProperties && expandProperties.length > 0) {
query = query.expand(expandProperties);
}
let callback = (error: GraphError, response: any, rawResponse?: any) => {
if (error) {
reject(error);
} else {
resolve(response);
}
};
await query.get(callback);
// tslint:disable-next-line: no-unused-expression
typeof(content) === "object" && (content = JSON.stringify(content));
// tslint:disable-next-line: no-unused-expression
selectProperties && selectProperties.length > 0 && (query = query.select(selectProperties));
// tslint:disable-next-line: no-unused-expression
filter && filter.length > 0 && (query = query.filter(filter));
// tslint:disable-next-line: no-unused-expression
expandProperties && expandProperties.length > 0 && (query = query.expand(expandProperties));
// tslint:disable-next-line: no-unused-expression
count && (query = query.count(count));
let callback = (error: GraphError, response: any, rawResponse?: any) => error ? reject(error) : resolve(response);
//* ES2016
["post", "patch"].includes(method) ? await query[method](content, callback) : await query[method](callback);
});
return p;
}
public static async Patch(apiUrl: string, version: string = "v1.0", content: any): Promise<any> {
var p = new Promise<string>(async (resolve, reject) => {
if (typeof (content) === "object") {
content = JSON.stringify(content);
}
let query = this._graphClient.api(apiUrl).version(version);
let callback = (error: GraphError, _response: any, rawResponse?: any) => {
if (error) {
reject(error);
} else {
resolve();
}
};
await query.update(content, callback);
});
return p;
}
public static async Post(apiUrl: string, version: string = "v1.0", content: any): Promise<any> {
var p = new Promise<string>(async (resolve, reject) => {
if (typeof (content) === "object") {
content = JSON.stringify(content);
}
let query = this._graphClient.api(apiUrl).version(version);
let callback = (error: GraphError, response: any, rawResponse?: any) => {
if (error) {
reject(error);
} else {
resolve(response);
}
};
await query.post(content, callback);
});
return p;
}
public static async Delete(apiUrl: string, version: string = "v1.0"): Promise<any> {
var p = new Promise<string>(async (resolve, reject) => {
let query = this._graphClient.api(apiUrl).version(version);
let callback = (error: GraphError, response: any, rawResponse?: any) => {
if (error) {
reject(error);
} else {
resolve(response);
}
};
await query.delete(callback);
});
return p;
}
}
}

View File

@ -0,0 +1,7 @@
import { IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { ICoordinates } from "@pnp/spfx-controls-react/lib/Map";
export interface ICMMDDropdownOption extends IDropdownOption {
data: ICoordinates;
parent?: string;
}

View File

@ -1,5 +0,0 @@
export interface IOption {
key: string;
text: string;
parent: string;
}

View File

@ -0,0 +1,4 @@
export interface IProperty {
key: string;
value: string;
}

View File

@ -1,5 +1,4 @@
import { IDescription } from "./IDescription";
import { ILabel } from "./ILabel";
import { IProperty, ILabel, IDescription } from ".";
export interface ITerm {
id: string;
@ -7,4 +6,5 @@ export interface ITerm {
lastModifiedDateTime: Date;
labels: ILabel[];
descriptions: IDescription[];
properties: IProperty[];
}

View File

@ -2,4 +2,5 @@ export { ILabel } from './ILabel';
export { IDescription } from './IDescription';
export { ITerm } from './ITerm';
export { ITerms } from './ITerms';
export { IOption } from './IOption';
export { IProperty } from './IProperty';
export { ICMMDDropdownOption } from './ICMMDDropdownOption';

View File

@ -1,5 +1,5 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json",
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
@ -19,20 +19,18 @@
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
"es2015.collection",
"es2015.promise",
"ES2016.Array.Include"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -1,5 +1,5 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
@ -17,7 +17,6 @@
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,