Initial commit

Added required files
This commit is contained in:
Anoop Tatti 2020-08-24 01:49:32 +01:00 committed by GitHub
parent 811af626f6
commit e1f38972ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 24316 additions and 73 deletions

View File

@ -1,73 +1,73 @@
# Cascading managed metadata using Graph API (beta) # Cascading managed metadata using Graph API (beta)
This webpart shows how to use the Microsoft Graph APIs (beta) for Taxonomy to get the data. This webpart shows how to use the Microsoft Graph APIs (beta) for Taxonomy to get the data.
### Functionality ### Functionality
![Cascading managed metadata](./assets/cmmd.gif) ![Cascading managed metadata](./assets/cmmd.gif)
### Termstore ### Termstore
![Term store](./assets/termstore.png) ![Term store](./assets/termstore.png)
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![SPFx v1.11.0](https://img.shields.io/badge/SPFx-1.11.0-green.svg) ![SPFx v1.11.0](https://img.shields.io/badge/SPFx-1.11.0-green.svg)
## Applies to ## Applies to
* [SharePoint Framework Developer](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview) * [SharePoint Framework Developer](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-developer-tenant) * [Office 365 developer tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-developer-tenant)
## Solution ## Solution
Solution|Author(s) Solution|Author(s)
--------|--------- --------|---------
react-graph-cascading-managed-metadata| Anoop Tatti ([@anooptells](https://twitter.com/anooptells)) react-graph-cascading-managed-metadata| Anoop Tatti ([@anooptells](https://twitter.com/anooptells))
## Version history ## Version history
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0.0|Aug 24, 2020|Initial release 1.0.0|Aug 24, 2020|Initial release
## Disclaimer ## 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.** **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.**
--- ---
## Features ## Features
This sample illustrates the following concepts on top of the SharePoint Framework: This sample illustrates the following concepts on top of the SharePoint Framework:
* Get data terms using Microsoft Graph API (beta). * Get data terms using Microsoft Graph API (beta).
* React Hooks * React Hooks
* Using async / await for the async calls * Using async / await for the async calls
* Caching the data in session storage * Caching the data in session storage
* Usage of PnP SPFx controls (Maps and Placeholder) * Usage of PnP SPFx controls (Maps and Placeholder)
* Office UI fabric components * Office UI fabric components
## Config ## Config
* Set up the termset structure as shown in the image above. * Set up the termset structure as shown in the image above.
* For the cities, get the required latitude and longitude. * For the cities, get the required latitude and longitude.
* Set the description of the city term as `latitude;longitude` (as highlighted for teh term `London` in the image above). * Set the description of the city term as `latitude;longitude` (as highlighted for teh term `London` in the image above).
## Enhancements ## Enhancements
* Currently, this webpart supports 2 level cascading. So there is scope to enhance this such that it supports more levels of cascading dynamically. * Currently, this webpart supports 2 level cascading. So there is scope to enhance this such that it supports more levels of cascading dynamically.
* Currently, this webpart 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. * Currently, this webpart 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.
### Building the code ### Building the code
```bash ```bash
git clone the repo git clone the repo
npm i npm i
npm i -g gulp npm i -g gulp
gulp gulp
``` ```
This package produces the following: This package produces the following:
* lib/* - intermediate-stage commonjs build artifacts * lib/* - intermediate-stage commonjs build artifacts
* dist/* - the bundled script, along with other resources * dist/* - the bundled script, along with other resources
* deploy/* - all resources which should be uploaded to a CDN. * deploy/* - all resources which should be uploaded to a CDN.

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"cascading-managed-metadata-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/cascadingManagedMetadata/CascadingManagedMetadataWebPart.js",
"manifest": "./src/webparts/cascadingManagedMetadata/CascadingManagedMetadataWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"CascadingManagedMetadataWebPartStrings": "lib/webparts/cascadingManagedMetadata/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/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-graph-cascading-managed-metadata",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-graph-cascading-managed-metadata-client-side-solution",
"id": "cdc626ca-e9a7-4d1d-bded-a574bc5e61d0",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-graph-cascading-managed-metadata.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,36 @@
'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");
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) { }
done();
});
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;
}
});
}
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
{
"name": "react-graph-cascading-managed-metadata",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"serve": "cross-env NODE_OPTIONS=--max_old_space_size=4096 gulp bundle --custom-serve && cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack-dev-server --mode development --config ./webpack.js --env.env=dev"
},
"dependencies": {
"@microsoft/sp-core-library": "1.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.19.0",
"office-ui-fabric-react": "6.214.0",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"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",
"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"
}
}

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,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "d3ff339d-201a-4f48-860c-0a3e435c0ad6",
"alias": "CascadingManagedMetadataWebPart",
"componentType": "WebPart",
// 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"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "cascadingManagedMetadata" },
"description": { "default": "cascadingManagedMetadata description" },
"officeFabricIconFontName": "Page",
"properties": {
"termSetId": ""
}
}]
}

View File

@ -0,0 +1,93 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, DisplayMode } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'CascadingManagedMetadataWebPartStrings';
import CascadingManagedMetadata from './components/CascadingManagedMetadata';
import { MSGraph } from './services/MSGraph';
export interface ICascadingManagedMetadataWebPartProps {
termSetId: string;
}
export default class CascadingManagedMetadataWebPart extends BaseClientSideWebPart<ICascadingManagedMetadataWebPartProps> {
private _placeholder = null;
public async render(): Promise<void> {
await MSGraph.Init(this.context);
let renderElement = null;
//TODO: Use function to check if GUID?
if (this.properties.termSetId && this.properties.termSetId.length == 36) {
renderElement = React.createElement(
CascadingManagedMetadata,
{
termSetId: this.properties.termSetId
}
);
} else {
if (this.displayMode === DisplayMode.Edit) {
const { Placeholder } = await import(
/* webpackChunkName: 'cascadingManagedMetadata-property-pane' */
'@pnp/spfx-controls-react/lib/Placeholder'
);
this._placeholder = Placeholder;
const placeholder: React.ReactElement<any> = React.createElement(
this._placeholder,
{
iconName: 'Edit',
iconText: 'Configure your webpart',
description: 'Please configure the web part.',
buttonLabel: 'Confifure',
onConfigure: this._onConfigure.bind(this)
}
);
renderElement = placeholder;
} else {
renderElement = React.createElement('div', null);
}
}
ReactDom.render(renderElement, this.domElement);
}
private _onConfigure = () => {
this.context.propertyPane.open();
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('termSetId', {
label: strings.TermSetIdFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,5 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.cascadingManagedMetadata {
}

View File

@ -0,0 +1,16 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'button': string;
'cascadingManagedMetadata': string;
'column': string;
'container': string;
'description': string;
'label': string;
'ms-Grid': string;
'row': string;
'subTitle': string;
'title': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,100 @@
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 { Dropdown, DropdownMenuItemType, IDropdownStyles, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { Map, ICoordinates, MapType } from "@pnp/spfx-controls-react/lib/Map";
import { MMDService } from '../services/MMDService';
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 [selectedCity, setSelectedCity] = React.useState<string>(null);
const [coordinates, setCoordinates] = React.useState<ICoordinates>({ latitude: null, longitude: null });
React.useEffect(() => {
_getCountries().then(terms => {
const options: IDropdownOption[] = terms.value.map(t => ({ key: t.id, text: t.labels[0].name }));
setCountriesList(options);
});
});
//* Get the country terms i.e. level 1 children using Graph
const _getCountries = async (): Promise<ITerms> => {
let countries: ITerms = await MSGraph.Get(`/termStore/sets/${props.termSetId}/children`, "beta");
return (countries);
};
//* Get the city terms under a country i.e. level 2 children using Graph
const _onCountryChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setCoordinates({ latitude: null, longitude: null });
setSelectedCityCoordinates(null);
setSelectedCity(null);
let countryTermId: string = item.key.toString();
MMDService.GetTermsAsDropdownOptions(`/termStore/sets/${props.termSetId}/terms/${countryTermId}/children`, countryTermId, true).then(options => {
setCitiesList(options);
});
};
//* 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);
};
return (
<div>
<Dropdown
label="Country"
placeHolder="Select a country"
options={countriesList}
onChange={_onCountryChange} />
<Dropdown
label="City"
selectedKey={selectedCityCoordinates}
placeHolder="Select a city"
options={citiesList}
onChange={_onCityChange} />
{
coordinates.latitude && coordinates.longitude ?
(
<React.Fragment>
<Map
titleText={`Map of our office in ${selectedCity}`}
coordinates={coordinates}
zoom={15}
enableSearch={false} />
</React.Fragment>
) :
(
selectedCity ?
<div style={{ marginTop: "10px" }}>
<MessageBar messageBarType={MessageBarType.warning}>To see the map, please check if the coordinates have been configured correctly.</MessageBar>
</div> :
<div style={{ marginTop: "10px" }}>
<MessageBar messageBarType={MessageBarType.warning}>To see the map, please select a city.</MessageBar>
</div>
)
}
</div>
);
};
export default CascadingManagedMetadata;

View File

@ -0,0 +1,3 @@
export interface ICascadingManagedMetadataProps {
termSetId: string;
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Properties",
"BasicGroupName": "Basic",
"TermSetIdFieldLabel": "Termset Id"
}
});

View File

@ -0,0 +1,10 @@
declare interface ICascadingManagedMetadataWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
TermSetIdFieldLabel: string;
}
declare module 'CascadingManagedMetadataWebPartStrings' {
const strings: ICascadingManagedMetadataWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,71 @@
import { ITerms, IOption } from "../../interfaces";
import { MSGraph } from "./MSGraph";
import { IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
export class MMDService {
private static _sessionStorageKey: string = "CMMD_Options";
public static async GetTermsAsDropdownOptions(apiUrl: string, parent: string, tryFromCache: boolean): Promise<IDropdownOption[]> {
let options: IDropdownOption[] = [];
if (tryFromCache) {
let optionsFromCache: IOption[] = 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;
} else {
//Get from Graph
return await this._getTermsAsDropdownOptionsUsingGraph(apiUrl, parent);
}
} else {
//Get from Graph
return await this._getTermsAsDropdownOptionsUsingGraph(apiUrl, parent);
}
} else {
return await this._getTermsAsDropdownOptionsUsingGraph(apiUrl, parent);
}
}
private static async _getTermsAsDropdownOptionsUsingGraph(apiUrl: string, parent: string): Promise<IDropdownOption[]> {
let terms: ITerms = await MSGraph.Get(apiUrl, "beta");
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,
parent
}));
let optionsFromCache: IOption[] = this._fetchFromSessionStorge();
optionsToStoreInCache = [...optionsFromCache, ...optionsToStoreInCache];
window.sessionStorage.setItem(this._sessionStorageKey, JSON.stringify(optionsToStoreInCache));
console.log("Options added in cache");
return options;
} else {
return [];
}
}
private static _fetchFromSessionStorge(): IOption[] {
let result: IOption[] = [];
let stringResult: string = window.sessionStorage.getItem(this._sessionStorageKey);
if (stringResult) {
try {
result = JSON.parse(stringResult);
if(result.length) {
console.log("Fetched options from cache");
}
} catch (error) {
console.error(error);
}
}
return result;
}
}

View File

@ -0,0 +1,88 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { GraphError } from '@microsoft/microsoft-graph-client';
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> {
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);
});
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,4 @@
export interface IDescription {
description: string;
languageTag: string;
}

View File

@ -0,0 +1,5 @@
export interface ILabel {
name: string;
isDefault: boolean;
languageTag: string;
}

View File

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

View File

@ -0,0 +1,10 @@
import { IDescription } from "./IDescription";
import { ILabel } from "./ILabel";
export interface ITerm {
id: string;
createdDateTime: Date;
lastModifiedDateTime: Date;
labels: ILabel[];
descriptions: IDescription[];
}

View File

@ -0,0 +1,5 @@
import { ITerm } from "./ITerm";
export interface ITerms {
value: ITerm[];
}

View File

@ -0,0 +1,5 @@
export { ILabel } from './ILabel';
export { IDescription } from './IDescription';
export { ITerm } from './ITerm';
export { ITerms } from './ITerms';
export { IOption } from './IOption';

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,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -0,0 +1,259 @@
const path = require("path");
const fs = require("fs");
const webpack = require("webpack");
const resolve = require("path").resolve;
const CertStore = require("@microsoft/gulp-core-build-serve/lib/CertificateStore");
const CertificateStore = CertStore.CertificateStore || CertStore.default;
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const del = require("del");
const port = 4321;
const host = "https://localhost:" + port;
///
// Transforms define("<guid>", ...) to web part specific define("<web part id_version", ...)
// the same approach is used inside copyAssets SPFx build step
///
class DynamicLibraryPlugin {
constructor(options) {
this.opitons = options;
}
apply(compiler) {
compiler.hooks.emit.tap("DynamicLibraryPlugin", compilation => {
for (const assetId in this.opitons.modulesMap) {
const moduleMap = this.opitons.modulesMap[assetId];
if (compilation.assets[assetId]) {
const rawValue = compilation.assets[assetId].children[0]._value;
compilation.assets[assetId].children[0]._value = rawValue.replace(this.opitons.libraryName, moduleMap.id + "_" + moduleMap.version);
}
}
});
}
}
///
// Removes *.module.scss.ts on the first execution in order prevent conflicts with *.module.scss.d.ts
// generated by css-modules-typescript-loader
///
class ClearCssModuleDefinitionsPlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
compiler.hooks.done.tap("FixStylesPlugin", stats => {
if (!this.options.deleted) {
setTimeout(() => {
del.sync(["src/**/*.module.scss.ts"]);
}, 3000);
this.options.deleted = true;
}
});
}
}
let baseConfig = {
target: "web",
mode: "development",
devtool: "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js"],
modules: ["node_modules"]
},
context: path.resolve(__dirname),
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
options: {
transpileOnly: true,
compilerOptions: {
declarationMap: false
}
},
exclude: /node_modules/
},
{
use: [{
loader: "@microsoft/loader-cased-file",
options: {
name: "[name:lower]_[hash].[ext]"
}
}],
test: /\.(jpe?g|png|woff|eot|ttf|svg|gif|dds)$/i
},
{
use: [{
loader: "html-loader"
}],
test: /\.html$/
},
{
test: /\.css$/,
use: [
{
loader: "@microsoft/loader-load-themed-styles",
options: {
async: true
}
},
{
loader: "css-loader"
}
]
},
{
test: function (fileName) {
return fileName.endsWith(".module.scss"); // scss modules support
},
use: [
{
loader: "@microsoft/loader-load-themed-styles",
options: {
async: true
}
},
"css-modules-typescript-loader",
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[local]_[hash:base64:8]"
}
}
}, // translates CSS into CommonJS
"sass-loader" // compiles Sass to CSS, using Node Sass by default
]
},
{
test: function (fileName) {
return !fileName.endsWith(".module.scss") && fileName.endsWith(".scss"); // just regular .scss
},
use: [
{
loader: "@microsoft/loader-load-themed-styles",
options: {
async: true
}
},
"css-loader", // translates CSS into CommonJS
"sass-loader" // compiles Sass to CSS, using Node Sass by default
]
}
]
},
plugins: [
new ForkTsCheckerWebpackPlugin({
tslint: true
}),
new ClearCssModuleDefinitionsPlugin(),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.DEBUG": JSON.stringify(true),
"DEBUG": JSON.stringify(true)
})],
devServer: {
hot: false,
contentBase: resolve(__dirname),
publicPath: host + "/dist/",
host: "localhost",
port: port,
disableHostCheck: true,
historyApiFallback: true,
open: true,
writeToDisk: false,
openPage: host + "/temp/workbench.html",
stats: {
preset: "errors-only",
colors: true,
chunks: false,
modules: false,
assets: false
},
proxy: { // url re-write for resources to be served directly from src folder
"/lib/**/loc/*.js": {
target: host,
pathRewrite: { "^/lib": "/src" },
secure: false
}
},
headers: {
"Access-Control-Allow-Origin": "*",
},
https: {
cert: CertificateStore.instance.certificateData,
key: CertificateStore.instance.keyData
}
},
}
const createConfig = function () {
// remove old css module TypeScript definitions
del.sync(["dist/*.js", "dist/*.map"]);
// we need only "externals", "output" and "entry" from the original webpack config
let originalWebpackConfig = require("./temp/_webpack_config.json");
baseConfig.externals = originalWebpackConfig.externals;
baseConfig.output = originalWebpackConfig.output;
baseConfig.entry = getEntryPoints(originalWebpackConfig.entry);
baseConfig.output.publicPath = host + "/dist/";
const manifest = require("./temp/manifests.json");
const modulesMap = {};
const originalEntries = Object.keys(originalWebpackConfig.entry);
for (const jsModule of manifest) {
if (jsModule.loaderConfig
&& jsModule.loaderConfig.entryModuleId
&& originalEntries.indexOf(jsModule.loaderConfig.entryModuleId) !== -1) {
modulesMap[jsModule.loaderConfig.entryModuleId + ".js"] = {
id: jsModule.id,
version: jsModule.version
}
}
}
baseConfig.plugins.push(new DynamicLibraryPlugin({
modulesMap: modulesMap,
libraryName: originalWebpackConfig.output.library
}));
return baseConfig;
}
function getEntryPoints(entry) {
// fix: ".js" entry needs to be ".ts"
// also replaces the path form /lib/* to /src/*
let newEntry = {};
let libSearchRegexp;
if (path.sep === "/") {
libSearchRegexp = /\/lib\//gi;
} else {
libSearchRegexp = /\\lib\\/gi;
}
const srcPathToReplace = path.sep + "src" + path.sep;
for (const key in entry) {
let entryPath = entry[key];
if (entryPath.indexOf("bundle-entries") === -1) {
entryPath = entryPath.replace(libSearchRegexp, srcPathToReplace).slice(0, -3) + ".ts";
} else {
// replace paths and extensions in bundle file
let bundleContent = fs.readFileSync(entryPath).toString();
bundleContent = bundleContent.replace(libSearchRegexp, srcPathToReplace).replace(/\.js/gi, ".ts");
fs.writeFileSync(entryPath, bundleContent);
}
newEntry[key] = entryPath;
}
return newEntry;
}
module.exports = createConfig();