Merge pull request #1259 from pschaeflein/master
This commit is contained in:
commit
7277688364
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.10.0",
|
||||
"libraryName": "react-adaptivecards-hooks",
|
||||
"libraryId": "9b520d32-ce30-4ffa-bf38-5d888e65c782",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
# react-adaptivecards-hooks
|
||||
|
||||
## Summary
|
||||
|
||||
A version of [react-adaptivecards](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-adaptivecards) using React Hooks.
|
||||
|
||||
![Adaptive Cards in SharePoint](assets/preview.png)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https:/dev.office.com/sharepoint)
|
||||
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-adaptivecards-hooks | Paul Schaeflein (http://www.schaeflein.net)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|April 27, 2020|Initial release
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
* Clone this repository
|
||||
* in the command line run:
|
||||
* `npm install`
|
||||
* `gulp serve`
|
||||
|
||||
### Code structure
|
||||
|
||||
| File | Type | Description |
|
||||
|------------------------------|--------------------------------------------------|----------------|
|
||||
| AdaptiveCardViewerWebPart.ts | React Class component (derives from BaseWebPart) | Used to define web part properties and bootstrap the component tree|
|
||||
| RootComponent.tsx | React Function component | Interrogates webpart properties and establishes AppContext and initial state.<br/>Monitors CardService state and dispatches updates to viewer state. |
|
||||
| AppContext.ts | React context provider | Exposes the SPFx webpart context, the webpart instance and the state dispatch to all components via `React.useContext()` |
|
||||
| CardService.ts | React Hook | Abstracts the SP HttpClient |
|
||||
| CardServiceReducer.ts | React Reducer | Reducer/state for CardService hook |
|
||||
| AdaptiveCardViewer.tsx | React Function component | Top-level UI component. |
|
||||
| AdaptiveCardHost.tsx | React Function component | Renders placeholder if template/data are missing. Handles card actions. |
|
||||
| AdaptiveCard.tsx | React Class component | Responsible for rendering adaptive card and expanding card with data |
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-adaptivecards-hooks" />
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"adaptive-card-viewer-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.js",
|
||||
"manifest": "./src/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"AdaptiveCardViewerWebPartStrings": "lib/webparts/adaptiveCardViewer/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-adaptivecards-hooks",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-adaptivecards-hooks",
|
||||
"title": "Adaptive Card Viewer",
|
||||
"iconPath": "assets/adaptive-cards.png",
|
||||
"id": "9b520d32-ce30-4ffa-bf38-5d888e65c782",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"isDomainIsolated": false
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-adaptivecards-hooks.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
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.`);
|
||||
|
||||
let copyPackageIcon = build.subTask('copy-package-icon', function (gulp, buildOptions, done) {
|
||||
this.log(`Inspecting package-solution.json for icon`);
|
||||
//Get the config file path
|
||||
let psConfigPath = path.join(process.cwd(), 'config', "package-solution.json");
|
||||
|
||||
//read the config file into a JSON object
|
||||
let psConfig = undefined;
|
||||
try {
|
||||
var content = fs.readFileSync(psConfigPath, 'utf8');
|
||||
psConfig = JSON.parse(content);
|
||||
}
|
||||
catch (e) {
|
||||
this.log(e);
|
||||
}
|
||||
|
||||
if (psConfig && psConfig.solution && psConfig.solution.iconPath) {
|
||||
// Copy to sharepoint folder so it is found by package-solution
|
||||
let src = `src/${psConfig.solution.iconPath}`;
|
||||
let dest = `sharepoint/${psConfig.solution.iconPath}`;
|
||||
this.log(`icon: ${src}`);
|
||||
this.log(`dest: ${dest}`);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.copyFileSync(src, dest);
|
||||
|
||||
// Copy to CDN staging
|
||||
gulp.src(`src/${psConfig.solution.iconPath}`)
|
||||
.pipe(gulp.dest('./temp/deploy'));
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
build.rig.addPostBundleTask(copyPackageIcon);
|
||||
|
||||
build.initialize(require('gulp'));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "react-adaptivecards-hooks",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.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/sp": "^2.0.4",
|
||||
"@pnp/spfx-controls-react": "^1.17.0",
|
||||
"@pnp/spfx-property-controls": "^1.17.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "16.8.8",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"adaptivecards": "^1.2.5",
|
||||
"adaptivecards-fabric": "^1.0.4",
|
||||
"adaptivecards-templating": "^0.1.1-alpha.1",
|
||||
"markdown-it": "^10.0.0",
|
||||
"office-ui-fabric-react": "6.189.2",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.10.0",
|
||||
"@microsoft/sp-tslint-rules": "1.10.0",
|
||||
"@microsoft/sp-module-interfaces": "1.10.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.10.0",
|
||||
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,232 @@
|
|||
import * as React from 'react';
|
||||
import { IAdaptiveCardProps } from './IAdaptiveCardProps';
|
||||
|
||||
import * as AdaptiveCards from "adaptivecards";
|
||||
import * as ACData from "adaptivecards-templating";
|
||||
import * as ACFabric from "adaptivecards-fabric";
|
||||
|
||||
// Support for theme and section color
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
import { IValidationError } from './IValidationError';
|
||||
import { IAdaptiveCardActionResult } from './IAdaptiveCardActionResult';
|
||||
|
||||
// Support for markdown
|
||||
import * as markdownit from "markdown-it";
|
||||
|
||||
|
||||
import { IAdaptiveCardState } from '.';
|
||||
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react';
|
||||
|
||||
// Localization
|
||||
import * as strings from 'AdaptiveCardViewerWebPartStrings';
|
||||
|
||||
/**
|
||||
* Displays an Adaptive Card
|
||||
* Supports templating and markdown syntax
|
||||
* Also adapts to changing environment colors
|
||||
*/
|
||||
export class AdaptiveCard extends React.Component<IAdaptiveCardProps, IAdaptiveCardState> {
|
||||
// The rendering container
|
||||
private _acContainer: HTMLDivElement;
|
||||
|
||||
constructor(props: IAdaptiveCardProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errors: []
|
||||
};
|
||||
}
|
||||
public componentDidMount(): void {
|
||||
this._renderAdaptiveCard();
|
||||
}
|
||||
|
||||
public componentDidUpdate(_prevProps: IAdaptiveCardProps, _prevState: {}): void {
|
||||
if (_prevProps != this.props) {
|
||||
// Pretty much any changes will result in a redraw.
|
||||
this._renderAdaptiveCard();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IAdaptiveCardProps> {
|
||||
return <>
|
||||
{this.state.errors.length > 0 &&
|
||||
<MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
|
||||
{strings.AdaptiveCardErrorIntro}<br />
|
||||
{this.state.errors.map((error: string) => {
|
||||
return <p>{error}</p>;
|
||||
})}
|
||||
</MessageBar>
|
||||
}
|
||||
<div className={this.props.className} ref={(elm) => { this._acContainer = elm; }}></div>
|
||||
</>;
|
||||
}
|
||||
|
||||
private _renderAdaptiveCard() {
|
||||
// There is nothing to render if we don't have a template (or nothing to render to)
|
||||
if (!this.props.template || !this._acContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let errors: Array<IValidationError> = [];
|
||||
let card: {};
|
||||
|
||||
if (this.props.data && this.props.useTemplating) {
|
||||
// Define a template payload
|
||||
var templatePayload = {};
|
||||
try {
|
||||
templatePayload = JSON.parse(this.props.template);
|
||||
} catch (error) {
|
||||
this._errorHandler(strings.TemplateJsonError + error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a Template instance from the template payload
|
||||
var template = new ACData.Template(templatePayload);
|
||||
|
||||
// Create a data binding context, and set its $root property to the
|
||||
// data object to bind the template to
|
||||
var context = new ACData.EvaluationContext();
|
||||
try {
|
||||
context.$root = JSON.parse(this.props.data);
|
||||
} catch (error) {
|
||||
this._errorHandler(strings.DataJsonError + error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand the card by combining the template and data
|
||||
card = template.expand(context);
|
||||
} else {
|
||||
try {
|
||||
card = JSON.parse(this.props.template);
|
||||
} catch (error) {
|
||||
this._errorHandler(strings.TemplateJsonError + error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an AdaptiveCard instance
|
||||
let adaptiveCard = new AdaptiveCards.AdaptiveCard();
|
||||
|
||||
// Use Fabric controls when rendering Adaptive Cards
|
||||
ACFabric.useFabricComponents();
|
||||
|
||||
// Get the semantic colors to adapt to changing section colors
|
||||
this._adjustThemeColors(adaptiveCard);
|
||||
|
||||
// Handle parsing markdown from HTML
|
||||
AdaptiveCards.AdaptiveCard.onProcessMarkdown = this._processMarkdownHandler;
|
||||
|
||||
// Set the adaptive card's event handlers. onExecuteAction is invoked
|
||||
// whenever an action is clicked in the card
|
||||
adaptiveCard.onExecuteAction = this._executeActionHandler;
|
||||
|
||||
// Parse the card payload
|
||||
adaptiveCard.parse(card, errors);
|
||||
|
||||
this.setState({
|
||||
errors: errors.map((error: IValidationError) => {
|
||||
return error.message;
|
||||
})
|
||||
});
|
||||
|
||||
// Empty the div so we can replace it
|
||||
while (this._acContainer.firstChild) {
|
||||
this._acContainer.removeChild(this._acContainer.lastChild);
|
||||
}
|
||||
|
||||
// Render the card to an HTML element:
|
||||
adaptiveCard.render(this._acContainer);
|
||||
}
|
||||
|
||||
private _executeActionHandler = (action) => {
|
||||
const actionType: string = action.getJsonTypeName();
|
||||
let url: string = action.getHref();
|
||||
|
||||
// Some Adaptive Cards templates wrap their Action.OpenUrl url parameter between parentheses.
|
||||
// strip them.
|
||||
// Maybe it means to open in a new window or something, but I can't find any reference to that in the specs.
|
||||
if (url) {
|
||||
// Only strip if the whole URL is wrapped with parentheses.
|
||||
if (url.charAt(0) === '(' && url.charAt(url.length - 1) === ')') {
|
||||
url = url.substr(1);
|
||||
url = url.substr(0, url.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const actionResult: IAdaptiveCardActionResult = {
|
||||
type: actionType,
|
||||
title: action.title,
|
||||
url: url,
|
||||
data: action.data
|
||||
};
|
||||
|
||||
this.props.onExecuteAction(actionResult);
|
||||
}
|
||||
|
||||
private _processMarkdownHandler = (md: string, result: any) => {
|
||||
// Don't stop parsing if there is invalid Markdown -- there's a lot of that in sample Adaptive Cards templates
|
||||
try {
|
||||
result.outputHtml = new markdownit().render(md);
|
||||
result.didProcess = true;
|
||||
} catch (error) {
|
||||
console.error('Error parsing Markdown', error);
|
||||
result.didProcess = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust Adaptive Card colors based on theme colors
|
||||
* @param adaptiveCard the Adaptive Cards for which you want to adjust the theme colors
|
||||
*/
|
||||
private _adjustThemeColors(adaptiveCard: AdaptiveCards.AdaptiveCard) {
|
||||
// Get the theme colors from the props -- if any
|
||||
if (this.props.themeVariant) {
|
||||
|
||||
const { semanticColors }: IReadonlyTheme = this.props.themeVariant;
|
||||
|
||||
// If there are theme colors, change the configuration to use these colors
|
||||
if (semanticColors) {
|
||||
// Set the hostConfig property unless you want to use the default Host Config
|
||||
// Host Config defines the style and behavior of a card
|
||||
|
||||
// I mapped as many theme colors as I could. Feel free to adjust the colours
|
||||
adaptiveCard.hostConfig = new AdaptiveCards.HostConfig({
|
||||
"separator": {
|
||||
"lineThickness": 1,
|
||||
"lineColor": semanticColors.bodyFrameDivider
|
||||
},
|
||||
"containerStyles": {
|
||||
"default": {
|
||||
"backgroundColor": semanticColors.bodyBackground,
|
||||
"foregroundColors": {
|
||||
"default": {
|
||||
"default": semanticColors.bodyText,
|
||||
"subtle": semanticColors.bodyTextChecked
|
||||
},
|
||||
"attention": {
|
||||
"default": semanticColors.errorText
|
||||
},
|
||||
"good": {
|
||||
"default": semanticColors['successText'] // for some reason, successText doesn't show up
|
||||
},
|
||||
"warning": {
|
||||
"default": semanticColors.warningText
|
||||
},
|
||||
"accent": {
|
||||
"default": semanticColors.accentButtonBackground
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _errorHandler(error: string) {
|
||||
this.setState({
|
||||
errors: [error]
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface IAdaptiveCardActionResult {
|
||||
type: string;
|
||||
title: string;
|
||||
data?: Object;
|
||||
url: string;
|
||||
method?: string;
|
||||
body?: string;
|
||||
headers?: Array<any>;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
import { IAdaptiveCardActionResult } from './IAdaptiveCardActionResult';
|
||||
|
||||
export interface IAdaptiveCardProps {
|
||||
themeVariant?: IReadonlyTheme | undefined;
|
||||
template: string;
|
||||
data: string;
|
||||
useTemplating: boolean;
|
||||
className?: string;
|
||||
onExecuteAction?: (action: IAdaptiveCardActionResult) => void;
|
||||
onParseSuccess?: () => void;
|
||||
onParseError?: (errors: Array<string>) => void;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IAdaptiveCardState {
|
||||
errors: Array<string>;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export interface IValidationError {
|
||||
error: ValidationError;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export declare enum ValidationError {
|
||||
Hint = 0,
|
||||
ActionTypeNotAllowed = 1,
|
||||
CollectionCantBeEmpty = 2,
|
||||
Deprecated = 3,
|
||||
ElementTypeNotAllowed = 4,
|
||||
InteractivityNotAllowed = 5,
|
||||
InvalidPropertyValue = 6,
|
||||
MissingCardType = 7,
|
||||
PropertyCantBeNull = 8,
|
||||
TooManyActions = 9,
|
||||
UnknownActionType = 10,
|
||||
UnknownElementType = 11,
|
||||
UnsupportedCardVersion = 12,
|
||||
DuplicateId = 13
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export * from './IValidationError';
|
||||
export * from './IAdaptiveCardProps';
|
||||
export * from './IAdaptiveCardState';
|
||||
export * from './AdaptiveCard';
|
||||
export * from './IAdaptiveCardActionResult';
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { MessageBar, MessageBarType, MessageBarButton } from 'office-ui-fabric-react';
|
||||
|
||||
import { IAdaptiveCardHostProps } from './IAdaptiveCardHostProps';
|
||||
import { AppContext } from '../../services/AppContext';
|
||||
import { AdaptiveCard } from '../AdaptiveCard/AdaptiveCard';
|
||||
import { IAdaptiveCardActionResult } from '../AdaptiveCard/IAdaptiveCardActionResult';
|
||||
import * as strings from 'AdaptiveCardViewerWebPartStrings';
|
||||
|
||||
export const AdaptiveCardHost: React.FunctionComponent<IAdaptiveCardHostProps> = (props) => {
|
||||
|
||||
const { spContext } = React.useContext(AppContext);
|
||||
|
||||
|
||||
/**
|
||||
* Demonstrates how we can respond to actions
|
||||
*/
|
||||
const _executeActionHandler = (action: IAdaptiveCardActionResult) => {
|
||||
console.log("Action", action);
|
||||
|
||||
// Feel free to handle actions any way you want
|
||||
switch (action.type) {
|
||||
case "Action.OpenUrl":
|
||||
window.open(action.url);
|
||||
break;
|
||||
case "Action.Submit":
|
||||
alert(action.title);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/** Opens the configuration pane */
|
||||
const _configureHandler = () => {
|
||||
spContext.propertyPane.open();
|
||||
};
|
||||
|
||||
|
||||
const { themeVariant, template, data, useTemplating } = props;
|
||||
|
||||
// if we didn't specify a template, we need a template!
|
||||
const needsTemplate: boolean = !template;
|
||||
|
||||
// If we use Adaptive Card Templating and didn't specify data, we need data!
|
||||
const needsData: boolean = useTemplating && !data;
|
||||
|
||||
// If we didn't use Adaptive Card Templating but the template contains $data nodes,
|
||||
// if means it is a data-enabled template
|
||||
const dataEnabledTemplate: boolean = template && template.indexOf('"$data"') > -1;
|
||||
|
||||
// If we didn't specify the template, show the placeholder
|
||||
if (needsTemplate) {
|
||||
return (
|
||||
<Placeholder iconName='Code'
|
||||
iconText={strings.PlaceholderIconText}
|
||||
description={strings.PlaceholderDescription}
|
||||
buttonLabel='Configure'
|
||||
onConfigure={_configureHandler} />
|
||||
);
|
||||
} else if (needsData) {
|
||||
// If we didn't specify data and we need it, display a different placeholder
|
||||
return (
|
||||
<Placeholder iconName='PageData'
|
||||
iconText={strings.DataNeededIconText}
|
||||
description={strings.DataNeededDescription}
|
||||
buttonLabel={strings.DataNeededButtonLabel}
|
||||
onConfigure={_configureHandler} />
|
||||
);
|
||||
}
|
||||
else {
|
||||
// Display the Adaptive Card
|
||||
return (
|
||||
<>
|
||||
{dataEnabledTemplate && !useTemplating && <MessageBar
|
||||
dismissButtonAriaLabel="Close"
|
||||
messageBarType={MessageBarType.warning}
|
||||
actions={
|
||||
<div>
|
||||
<MessageBarButton onClick={_configureHandler}>{strings.ConfigureButtonLabel}</MessageBarButton>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{strings.AdaptiveTemplatingWarningIntro}<a href={strings.AdaptiveCardTemplatingMoreInfoLinkUrl} target='_blank'>{strings.AdaptiveCardTemplating}</a>{strings.AdaptiveCardWarningPartTwo}<strong>{strings.UseAdaptiveTemplatingLabel}</strong>{strings.AdaptiveTemplatingEnd}
|
||||
</MessageBar>
|
||||
}
|
||||
<AdaptiveCard
|
||||
template={template}
|
||||
data={data}
|
||||
useTemplating={useTemplating}
|
||||
themeVariant={themeVariant}
|
||||
onExecuteAction={_executeActionHandler}
|
||||
className="tbd"
|
||||
/></>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
export interface IAdaptiveCardHostProps {
|
||||
themeVariant: IReadonlyTheme | undefined;
|
||||
template: string;
|
||||
data: string;
|
||||
useTemplating: boolean;
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import { IWebPartContext, IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';
|
||||
import { ISPView } from './ISPView';
|
||||
|
||||
|
||||
/**
|
||||
* Enum for specifying how the views should be sorted
|
||||
*/
|
||||
export enum PropertyFieldViewPickerOrderBy {
|
||||
Id = 1,
|
||||
Title
|
||||
}
|
||||
|
||||
/**
|
||||
* Public properties of the PropertyFieldViewPicker custom field
|
||||
*/
|
||||
export interface IPropertyFieldViewPickerProps {
|
||||
/**
|
||||
* Context of the current web part
|
||||
*/
|
||||
context: IWebPartContext;
|
||||
|
||||
/**
|
||||
* Custom Field will start to validate after users stop typing for `deferredValidationTime` milliseconds.
|
||||
* Default value is 200.
|
||||
*/
|
||||
deferredValidationTime?: number;
|
||||
|
||||
/**
|
||||
* Whether the property pane field is enabled or not.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* Filter views from Odata query
|
||||
*/
|
||||
filter?: string;
|
||||
|
||||
/**
|
||||
* An UNIQUE key indicates the identity of this control
|
||||
*/
|
||||
key?: string;
|
||||
|
||||
/**
|
||||
* Property field label displayed on top
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The List Id of the list where you want to get the views
|
||||
*/
|
||||
listId?: string;
|
||||
|
||||
/**
|
||||
* Specify the property on which you want to order the retrieve set of views.
|
||||
*/
|
||||
orderBy?: PropertyFieldViewPickerOrderBy;
|
||||
|
||||
/**
|
||||
* Parent Web Part properties
|
||||
*/
|
||||
properties: any;
|
||||
|
||||
/**
|
||||
* Initial selected view of the control
|
||||
*/
|
||||
selectedView?: string | string[];
|
||||
|
||||
/**
|
||||
* Defines view titles which should be excluded from the view picker control
|
||||
*/
|
||||
viewsToExclude?: string[];
|
||||
|
||||
/**
|
||||
* Absolute Web Url of target site (user requires permissions)
|
||||
*/
|
||||
webAbsoluteUrl?: string;
|
||||
|
||||
/**
|
||||
* The method is used to get the validation error message and determine whether the input value is valid or not.
|
||||
*
|
||||
* When it returns string:
|
||||
* - If valid, it returns empty string.
|
||||
* - If invalid, it returns the error message string and the text field will
|
||||
* show a red border and show an error message below the text field.
|
||||
*
|
||||
* When it returns Promise<string>:
|
||||
* - The resolved value is display as error message.
|
||||
* - The rejected, the value is thrown away.
|
||||
*
|
||||
*/
|
||||
onGetErrorMessage?: (value: string) => string | Promise<string>;
|
||||
/**
|
||||
* Defines a onPropertyChange function to raise when the selected value changed.
|
||||
* Normally this function must be always defined with the 'this.onPropertyChange'
|
||||
* method of the web part object.
|
||||
*/
|
||||
onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void;
|
||||
/**
|
||||
* Callback that is called before the dropdown is populated
|
||||
*/
|
||||
onViewsRetrieved?: (views: ISPView[]) => PromiseLike<ISPView[]> | ISPView[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Private properties of the PropertyFieldViewPicker custom field.
|
||||
* We separate public & private properties to include onRender & onDispose method waited
|
||||
* by the PropertyFieldCustom, without asking to the developer to add it when he's using
|
||||
* the PropertyFieldViewPicker.
|
||||
*/
|
||||
export interface IPropertyFieldViewPickerPropsInternal extends IPropertyFieldViewPickerProps, IPropertyPaneCustomFieldProps {
|
||||
context: IWebPartContext;
|
||||
deferredValidationTime?: number;
|
||||
disabled?: boolean;
|
||||
filter?: string;
|
||||
key: string;
|
||||
label: string;
|
||||
listId?: string;
|
||||
orderBy?: PropertyFieldViewPickerOrderBy;
|
||||
properties: any;
|
||||
selectedView?: string;
|
||||
targetProperty: string;
|
||||
viewsToExclude?: string[];
|
||||
webAbsoluteUrl?: string;
|
||||
onGetErrorMessage?: (value: string | string[]) => string | Promise<string>;
|
||||
onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void;
|
||||
onViewsRetrieved?: (views: ISPView[]) => PromiseLike<ISPView[]> | ISPView[];
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { IPropertyFieldViewPickerPropsInternal } from './IPropertyFieldViewPicker';
|
||||
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
|
||||
|
||||
/**
|
||||
* PropertyFieldViewPickerHost properties interface
|
||||
*/
|
||||
export interface IPropertyFieldViewPickerHostProps extends IPropertyFieldViewPickerPropsInternal {
|
||||
onChange: (targetProperty?: string, newValue?: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* PropertyFieldViewPickerHost state interface
|
||||
*/
|
||||
export interface IPropertyFieldViewPickerHostState {
|
||||
|
||||
results: IDropdownOption[];
|
||||
selectedKey?: string;
|
||||
errorMessage?: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface ISPView {
|
||||
Id: string;
|
||||
Title: string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ISPView } from ".";
|
||||
|
||||
/**
|
||||
* Defines a collection of SharePoint list views
|
||||
*/
|
||||
export interface ISPViews {
|
||||
|
||||
value: ISPView[];
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import {
|
||||
IPropertyPaneField,
|
||||
PropertyPaneFieldType,
|
||||
IWebPartContext
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import PropertyFieldViewPickerHost from './PropertyFieldViewPickerHost';
|
||||
import { IPropertyFieldViewPickerHostProps } from './IPropertyFieldViewPickerHost';
|
||||
import { PropertyFieldViewPickerOrderBy, IPropertyFieldViewPickerProps, IPropertyFieldViewPickerPropsInternal } from './IPropertyFieldViewPicker';
|
||||
import { ISPView } from '.';
|
||||
|
||||
/**
|
||||
* Represents a PropertyFieldViewPicker object
|
||||
*/
|
||||
class PropertyFieldViewPickerBuilder implements IPropertyPaneField<IPropertyFieldViewPickerPropsInternal> {
|
||||
|
||||
//Properties defined by IPropertyPaneField
|
||||
public properties: IPropertyFieldViewPickerPropsInternal;
|
||||
public targetProperty: string;
|
||||
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||
|
||||
//Custom properties label: string;
|
||||
private context: IWebPartContext;
|
||||
private label: string;
|
||||
private listId?: string;
|
||||
private orderBy: PropertyFieldViewPickerOrderBy;
|
||||
private selectedView: string;
|
||||
private viewsToExclude: string[];
|
||||
|
||||
private customProperties: any;
|
||||
private deferredValidationTime: number = 200;
|
||||
private disabled: boolean = false;
|
||||
private disableReactivePropertyChanges: boolean = false;
|
||||
private filter: string;
|
||||
private key: string;
|
||||
private webAbsoluteUrl?: string;
|
||||
private onGetErrorMessage: (value: string) => string | Promise<string>;
|
||||
private onViewsRetrieved?: (views: ISPView[]) => PromiseLike<ISPView[]> | ISPView[];
|
||||
public onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void { }
|
||||
private renderWebPart: () => void;
|
||||
|
||||
/**
|
||||
* Constructor method
|
||||
*/
|
||||
public constructor(_targetProperty: string, _properties: IPropertyFieldViewPickerPropsInternal) {
|
||||
this.render = this.render.bind(this);
|
||||
this.targetProperty = _targetProperty;
|
||||
this.properties = _properties;
|
||||
this.properties.onDispose = this.dispose;
|
||||
this.properties.onRender = this.render;
|
||||
this.label = _properties.label;
|
||||
this.context = _properties.context;
|
||||
this.webAbsoluteUrl = _properties.webAbsoluteUrl;
|
||||
this.listId = _properties.listId;
|
||||
this.selectedView = _properties.selectedView;
|
||||
this.orderBy = _properties.orderBy;
|
||||
this.onPropertyChange = _properties.onPropertyChange;
|
||||
this.customProperties = _properties.properties;
|
||||
this.key = _properties.key;
|
||||
this.viewsToExclude = _properties.viewsToExclude;
|
||||
this.filter = _properties.filter;
|
||||
this.onGetErrorMessage = _properties.onGetErrorMessage;
|
||||
this.onViewsRetrieved = _properties.onViewsRetrieved;
|
||||
|
||||
if (_properties.disabled === true) {
|
||||
this.disabled = _properties.disabled;
|
||||
}
|
||||
if (_properties.deferredValidationTime) {
|
||||
this.deferredValidationTime = _properties.deferredValidationTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the SPViewPicker field content
|
||||
*/
|
||||
private render(elem: HTMLElement, ctx?: any, changeCallback?: (targetProperty?: string, newValue?: any) => void): void {
|
||||
const componentProps = {
|
||||
label: this.label,
|
||||
targetProperty: this.targetProperty,
|
||||
context: this.context,
|
||||
webAbsoluteUrl: this.webAbsoluteUrl,
|
||||
listId: this.listId,
|
||||
orderBy: this.orderBy,
|
||||
onDispose: this.dispose,
|
||||
onRender: this.render,
|
||||
onChange: changeCallback,
|
||||
onPropertyChange: this.onPropertyChange,
|
||||
properties: this.customProperties,
|
||||
key: this.key,
|
||||
disabled: this.disabled,
|
||||
onGetErrorMessage: this.onGetErrorMessage,
|
||||
deferredValidationTime: this.deferredValidationTime,
|
||||
viewsToExclude: this.viewsToExclude,
|
||||
filter: this.filter,
|
||||
onViewsRetrieved: this.onViewsRetrieved
|
||||
};
|
||||
|
||||
// Single selector
|
||||
componentProps['selectedView'] = this.selectedView;
|
||||
const element: React.ReactElement<IPropertyFieldViewPickerHostProps> = React.createElement(PropertyFieldViewPickerHost, componentProps);
|
||||
// Calls the REACT content generator
|
||||
ReactDom.render(element, elem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the current object
|
||||
*/
|
||||
private dispose(_elem: HTMLElement): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a SPView Picker on the PropertyPane.
|
||||
* @param targetProperty - Target property the SharePoint view picker is associated to.
|
||||
* @param properties - Strongly typed SPView Picker properties.
|
||||
*/
|
||||
export function PropertyFieldViewPicker(targetProperty: string, properties: IPropertyFieldViewPickerProps): IPropertyPaneField<IPropertyFieldViewPickerPropsInternal> {
|
||||
//Create an internal properties object from the given properties
|
||||
const newProperties: IPropertyFieldViewPickerPropsInternal = {
|
||||
label: properties.label,
|
||||
targetProperty: targetProperty,
|
||||
context: properties.context,
|
||||
listId: properties.listId,
|
||||
selectedView: typeof properties.selectedView === 'string' ? properties.selectedView : null,
|
||||
onPropertyChange: properties.onPropertyChange,
|
||||
properties: properties.properties,
|
||||
onDispose: null,
|
||||
onRender: null,
|
||||
key: properties.key,
|
||||
disabled: properties.disabled,
|
||||
viewsToExclude: properties.viewsToExclude,
|
||||
filter: properties.filter,
|
||||
onGetErrorMessage: properties.onGetErrorMessage,
|
||||
deferredValidationTime: properties.deferredValidationTime,
|
||||
onViewsRetrieved: properties.onViewsRetrieved
|
||||
};
|
||||
//Calls the PropertyFieldViewPicker builder object
|
||||
//This object will simulate a PropertyFieldCustom to manage his rendering process
|
||||
return new PropertyFieldViewPickerBuilder(targetProperty, newProperties);
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
import * as React from 'react';
|
||||
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
|
||||
import { Async } from 'office-ui-fabric-react/lib/Utilities';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { IPropertyFieldViewPickerHostProps, IPropertyFieldViewPickerHostState } from './IPropertyFieldViewPickerHost';
|
||||
import { SPViewPickerService } from '../../services/SPViewPickerService';
|
||||
import FieldErrorMessage from '@pnp/spfx-property-controls/lib/propertyFields/errorMessage/FieldErrorMessage';
|
||||
import { ISPView } from '.';
|
||||
import { ISPViews } from './ISPViews';
|
||||
|
||||
// Empty view value
|
||||
const EMPTY_VIEW_KEY = 'NO_VIEW_SELECTED';
|
||||
|
||||
/**
|
||||
* Renders the controls for PropertyFieldViewPicker component
|
||||
*/
|
||||
export default class PropertyFieldViewPickerHost extends React.Component<IPropertyFieldViewPickerHostProps, IPropertyFieldViewPickerHostState> {
|
||||
private options: IDropdownOption[] = [];
|
||||
private selectedKey: string;
|
||||
private latestValidateValue: string;
|
||||
private async: Async;
|
||||
private delayedValidate: (value: string) => void;
|
||||
|
||||
/**
|
||||
* Constructor method
|
||||
*/
|
||||
constructor(props: IPropertyFieldViewPickerHostProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
results: this.options,
|
||||
errorMessage: ''
|
||||
};
|
||||
|
||||
this.async = new Async(this);
|
||||
this.validate = this.validate.bind(this);
|
||||
this.onChanged = this.onChanged.bind(this);
|
||||
this.notifyAfterValidate = this.notifyAfterValidate.bind(this);
|
||||
this.delayedValidate = this.async.debounce(this.validate, this.props.deferredValidationTime);
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
// Start retrieving the list views
|
||||
this.loadViews();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IPropertyFieldViewPickerHostProps, _prevState: IPropertyFieldViewPickerHostState): void {
|
||||
if (this.props.listId !== prevProps.listId || this.props.webAbsoluteUrl !== prevProps.webAbsoluteUrl) {
|
||||
this.loadViews();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the views from a SharePoint list
|
||||
*/
|
||||
private loadViews(): void {
|
||||
const viewService: SPViewPickerService = new SPViewPickerService(this.props, this.props.context);
|
||||
const viewsToExclude: string[] = this.props.viewsToExclude || [];
|
||||
this.options = [];
|
||||
viewService.getViews().then((response: ISPViews) => {
|
||||
// Start mapping the views that are selected
|
||||
response.value.forEach((view: ISPView) => {
|
||||
if (this.props.selectedView === view.Id) {
|
||||
this.selectedKey = view.Id;
|
||||
}
|
||||
|
||||
// Make sure that the current view is NOT in the 'viewsToExclude' array
|
||||
if (viewsToExclude.indexOf(view.Title) === -1 && viewsToExclude.indexOf(view.Id) === -1) {
|
||||
this.options.push({
|
||||
key: view.Id,
|
||||
text: view.Title
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Option to unselect the view
|
||||
this.options.unshift({
|
||||
key: EMPTY_VIEW_KEY,
|
||||
text: ''
|
||||
});
|
||||
|
||||
// Update the current component state
|
||||
this.setState({
|
||||
results: this.options,
|
||||
selectedKey: this.selectedKey
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises when a view has been selected
|
||||
*/
|
||||
private onChanged(option: IDropdownOption, _index?: number): void {
|
||||
const newValue: string = option.key as string;
|
||||
this.delayedValidate(newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the new custom field value
|
||||
*/
|
||||
private validate(value: string): void {
|
||||
if (this.props.onGetErrorMessage === null || this.props.onGetErrorMessage === undefined) {
|
||||
this.notifyAfterValidate(this.props.selectedView, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.latestValidateValue === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.latestValidateValue = value;
|
||||
|
||||
const errResult: string | PromiseLike<string> = this.props.onGetErrorMessage(value || '');
|
||||
if (typeof errResult !== 'undefined') {
|
||||
if (typeof errResult === 'string') {
|
||||
if (errResult === '') {
|
||||
this.notifyAfterValidate(this.props.selectedView, value);
|
||||
}
|
||||
this.setState({
|
||||
errorMessage: errResult
|
||||
});
|
||||
} else {
|
||||
errResult.then((errorMessage: string) => {
|
||||
if (!errorMessage) {
|
||||
this.notifyAfterValidate(this.props.selectedView, value);
|
||||
}
|
||||
this.setState({
|
||||
errorMessage: errorMessage
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.notifyAfterValidate(this.props.selectedView, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the parent Web Part of a property value change
|
||||
*/
|
||||
private notifyAfterValidate(oldValue: string, newValue: string) {
|
||||
// Check if the user wanted to unselect the view
|
||||
const propValue = newValue === EMPTY_VIEW_KEY ? '' : newValue;
|
||||
|
||||
// Deselect all options
|
||||
this.options = this.state.results.map(option => {
|
||||
if (option.selected) {
|
||||
option.selected = false;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
// Set the current selected key
|
||||
this.selectedKey = newValue;
|
||||
// Update the state
|
||||
this.setState({
|
||||
selectedKey: this.selectedKey,
|
||||
results: this.options
|
||||
});
|
||||
|
||||
if (this.props.onPropertyChange && propValue !== null) {
|
||||
// Store the new property value
|
||||
this.props.properties[this.props.targetProperty] = propValue;
|
||||
|
||||
// Trigger the default onPropertyChange event
|
||||
this.props.onPropertyChange(this.props.targetProperty, oldValue, propValue);
|
||||
|
||||
// Trigger the apply button
|
||||
if (typeof this.props.onChange !== 'undefined' && this.props.onChange !== null) {
|
||||
this.props.onChange(this.props.targetProperty, propValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the component will unmount
|
||||
*/
|
||||
public componentWillUnmount() {
|
||||
if (typeof this.async !== 'undefined') {
|
||||
this.async.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the SPViewPicker controls with Office UI Fabric
|
||||
*/
|
||||
public render(): JSX.Element {
|
||||
// Renders content
|
||||
return (
|
||||
<div>
|
||||
{this.props.label && <Label>{this.props.label}</Label>}
|
||||
<Dropdown
|
||||
disabled={this.props.disabled}
|
||||
label=''
|
||||
onChanged={this.onChanged}
|
||||
options={this.state.results}
|
||||
selectedKey={this.state.selectedKey}
|
||||
/>
|
||||
|
||||
<FieldErrorMessage errorMessage={this.state.errorMessage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export * from './PropertyFieldViewPicker';
|
||||
export * from './IPropertyFieldViewPicker';
|
||||
export * from './PropertyFieldViewPickerHost';
|
||||
export * from './IPropertyFieldViewPickerHost';
|
||||
export * from './ISPView';
|
||||
export * from './ISPViews';
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,15 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { ISPEventObserver } from '@microsoft/sp-core-library';
|
||||
import { IAdaptiveCardViewerState, AdaptiveCardViewerStateAction } from '../webparts/adaptiveCardViewer/components/IAdaptiveCardViewerState';
|
||||
|
||||
export interface IAppContextProps {
|
||||
spContext: WebPartContext;
|
||||
spEventObserver: ISPEventObserver;
|
||||
acViewerState: IAdaptiveCardViewerState;
|
||||
acViewerStateDispatch: React.Dispatch<AdaptiveCardViewerStateAction>;
|
||||
}
|
||||
|
||||
export const AppContext = createContext<IAppContextProps>(undefined);
|
||||
|
||||
export const getContentPackManagerState = () => useContext(AppContext);
|
|
@ -0,0 +1,58 @@
|
|||
import * as React from 'react';
|
||||
import { HttpClient, HttpClientConfiguration, HttpClientResponse } from '@microsoft/sp-http';
|
||||
import { cardServiceReducer, ICardServiceState } from './CardServiceReducer';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
|
||||
export const useCardService = (spContext: WebPartContext) => {
|
||||
const initialState: ICardServiceState = { type: 'status', isLoading: false, isError: false };
|
||||
|
||||
const [cardServiceState, dispatch] = React.useReducer(cardServiceReducer, initialState);
|
||||
|
||||
// make the call
|
||||
const fetchData = async (url: string, dataType: 'template'|'data') => {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
spContext.httpClient.get(url, HttpClient.configurations.v1)
|
||||
.then((response: HttpClientResponse) => {
|
||||
if (response.ok) {
|
||||
response.json()
|
||||
.then((data: any) => {
|
||||
if (dataType === 'template') {
|
||||
dispatch({ type: 'success_template', results: { template: JSON.stringify(data) } });
|
||||
}
|
||||
if (dataType === 'data') {
|
||||
dispatch({ type: 'success_data', results: { data: JSON.stringify(data) } });
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
dispatch({ type: 'failure', error: error });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const getAdaptiveCardJSON = async (templateUrl: string) => {
|
||||
dispatch({ type: 'request_template' });
|
||||
await fetchData(templateUrl,'template');
|
||||
|
||||
return () => {
|
||||
// clean up (equivalent to finally/dispose)
|
||||
};
|
||||
};
|
||||
|
||||
const getDataJSON = async (dataUrl: string) => {
|
||||
dispatch({ type: 'request_data' });
|
||||
await fetchData(dataUrl, 'data');
|
||||
|
||||
return () => {
|
||||
// clean up (equivalent to finally/dispose)
|
||||
};
|
||||
};
|
||||
|
||||
// return the items that consumers need
|
||||
return { cardServiceState, getAdaptiveCardJSON, getDataJSON};
|
||||
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
|
||||
|
||||
// Typing a useReducer React hook in TypeScript
|
||||
// https://codewithstyle.info/Typing-a-useReducer-React-hook-in-TypeScript/
|
||||
|
||||
type CardServiceDataType = 'template' | 'data';
|
||||
|
||||
export type ICardServiceState =
|
||||
| { type: 'complete', isLoading: boolean, isError: boolean, dataType: CardServiceDataType, data?: string }
|
||||
| { type: 'status', isLoading: boolean, isError: boolean, status?: { message: string, diagnostics: any } };
|
||||
|
||||
export type CardServiceAction =
|
||||
| { type: 'request_template' }
|
||||
| { type: 'success_template', results: { template: string } }
|
||||
| { type: 'request_data' }
|
||||
| { type: 'success_data', results: { data: string } }
|
||||
| { type: 'success_nodata', status: { message: string, diagnostics: any } }
|
||||
| { type: 'failure', error: any };
|
||||
|
||||
|
||||
export const cardServiceReducer: React.Reducer<ICardServiceState, CardServiceAction> = (state: ICardServiceState, action: CardServiceAction) => {
|
||||
console.log(`cardServiceReducer: ${action.type}`);
|
||||
|
||||
if (action.type === 'failure') {
|
||||
console.error(action.error);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'request_template':
|
||||
return {
|
||||
...state,
|
||||
type: 'status',
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
status: { message: "Requesting template", diagnostics: null }
|
||||
};
|
||||
case 'success_template':
|
||||
return {
|
||||
...state,
|
||||
type: "complete",
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
dataType: 'template',
|
||||
data: action.results.template
|
||||
};
|
||||
case 'request_data':
|
||||
return {
|
||||
...state,
|
||||
type: 'status',
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
status: { message: "Requesting data", diagnostics: null }
|
||||
};
|
||||
case 'success_data':
|
||||
return {
|
||||
...state,
|
||||
type: "complete",
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
dataType: "data",
|
||||
data: action.results.data
|
||||
};
|
||||
case 'success_nodata':
|
||||
return {
|
||||
...state,
|
||||
type: "status",
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
status: { message: "Operation successful", diagnostics: null }
|
||||
};
|
||||
case 'failure':
|
||||
return {
|
||||
...state,
|
||||
type: 'status',
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
status: { message: "failure", diagnostics: action.error }
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react";
|
||||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
|
||||
// Used to retrieve SharePoint items
|
||||
import { sp } from '@pnp/sp';
|
||||
import '@pnp/sp/webs';
|
||||
import '@pnp/sp/lists';
|
||||
import "@pnp/sp/views";
|
||||
|
||||
import { ICardServiceState, cardServiceReducer } from "../CardService/CardServiceReducer";
|
||||
|
||||
export const useSPListDataService = (spContext: WebPartContext) => {
|
||||
const initialState: ICardServiceState = { type: 'status', isLoading: false, isError: false };
|
||||
|
||||
const [listServiceState, dispatch] = React.useReducer(cardServiceReducer, initialState);
|
||||
|
||||
const fetchData = async (listId: string, viewId: string) => {
|
||||
sp.setup({
|
||||
spfxContext: spContext
|
||||
});
|
||||
|
||||
// Get the list
|
||||
const list = await sp.web.lists.getById(listId);
|
||||
const view:any = await list.getView(viewId);
|
||||
const _viewSchema = view.HtmlSchemaXml;
|
||||
|
||||
// Get the data as returned by the view
|
||||
const { Row: data } = await list.renderListDataAsStream({
|
||||
ViewXml: _viewSchema
|
||||
});
|
||||
|
||||
dispatch({ type: 'success_data', results: { data: JSON.stringify(data) } });
|
||||
};
|
||||
|
||||
const getListItems = async (listId: string, viewId: string) => {
|
||||
dispatch({ type: 'request_data' });
|
||||
await fetchData(listId, viewId);
|
||||
|
||||
return () => {
|
||||
// clean up (equivalent to finally/dispose)
|
||||
};
|
||||
};
|
||||
|
||||
// return the items that consumers need
|
||||
return { listServiceState, getListItems };
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { ISPViews } from "../../controls/PropertyFieldViewPicker";
|
||||
|
||||
export interface ISPViewPickerService {
|
||||
getViews(): Promise<ISPViews>;
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { SPHttpClient } from '@microsoft/sp-http';
|
||||
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { ISPView, IPropertyFieldViewPickerHostProps, PropertyFieldViewPickerOrderBy } from '../../controls/PropertyFieldViewPicker';
|
||||
import { ISPViewPickerService } from './ISPViewPickerService';
|
||||
import { ISPViews } from '../../controls/PropertyFieldViewPicker/ISPViews';
|
||||
|
||||
/**
|
||||
* Service implementation to get list & list items from current SharePoint site
|
||||
*/
|
||||
export class SPViewPickerService implements ISPViewPickerService {
|
||||
private context: IWebPartContext;
|
||||
private props: IPropertyFieldViewPickerHostProps;
|
||||
|
||||
/**
|
||||
* Service constructor
|
||||
*/
|
||||
constructor(_props: IPropertyFieldViewPickerHostProps, pageContext: IWebPartContext) {
|
||||
this.props = _props;
|
||||
this.context = pageContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the collection of view for a selected list
|
||||
*/
|
||||
public async getViews(): Promise<ISPViews> {
|
||||
if (Environment.type === EnvironmentType.Local) {
|
||||
// If the running environment is local, load the data from the mock
|
||||
return this.getViewsFromMock();
|
||||
}
|
||||
else {
|
||||
if (this.props.listId === undefined || this.props.listId === "") {
|
||||
return this.getEmptyViews();
|
||||
}
|
||||
|
||||
const webAbsoluteUrl = this.props.webAbsoluteUrl ? this.props.webAbsoluteUrl : this.context.pageContext.web.absoluteUrl;
|
||||
|
||||
// If the running environment is SharePoint, request the lists REST service
|
||||
let queryUrl: string = `${webAbsoluteUrl}/_api/lists(guid'${this.props.listId}')/Views?$select=Title,Id`;
|
||||
|
||||
// Check if the orderBy property is provided
|
||||
if (this.props.orderBy !== null) {
|
||||
queryUrl += '&$orderby=';
|
||||
switch (this.props.orderBy) {
|
||||
case PropertyFieldViewPickerOrderBy.Id:
|
||||
queryUrl += 'Id';
|
||||
break;
|
||||
case PropertyFieldViewPickerOrderBy.Title:
|
||||
queryUrl += 'Title';
|
||||
break;
|
||||
}
|
||||
|
||||
// Adds an OData Filter to the list
|
||||
if (this.props.filter){
|
||||
queryUrl += `&$filter=${encodeURIComponent(this.props.filter)}`;
|
||||
}
|
||||
|
||||
let response = await this.context.spHttpClient.get(queryUrl, SPHttpClient.configurations.v1);
|
||||
|
||||
let views = (await response.json()) as ISPViews;
|
||||
|
||||
// Check if onViewsRetrieved callback is defined
|
||||
if (this.props.onViewsRetrieved) {
|
||||
//Call onViewsRetrieved
|
||||
let lr = this.props.onViewsRetrieved(views.value);
|
||||
let output: ISPView[];
|
||||
|
||||
//Conditional checking to see of PromiseLike object or array
|
||||
if (lr instanceof Array) {
|
||||
output = lr;
|
||||
} else {
|
||||
output = await lr;
|
||||
}
|
||||
|
||||
views.value = output;
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty view for when a list isn't selected
|
||||
*/
|
||||
private getEmptyViews(): Promise<ISPViews> {
|
||||
return new Promise<ISPViews>((resolve) => {
|
||||
const listData: ISPViews = {
|
||||
value:[
|
||||
]
|
||||
};
|
||||
|
||||
resolve(listData);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns 3 fake SharePoint views for the Mock mode
|
||||
*/
|
||||
private getViewsFromMock(): Promise<ISPViews> {
|
||||
return new Promise<ISPViews>((resolve) => {
|
||||
const listData: ISPViews = {
|
||||
value:[
|
||||
{ Title: 'Mock View One', Id: '3bacd87b-b7df-439a-bb20-4d4d13523431' },
|
||||
{ Title: 'Mock View Two', Id: '5e37c820-e2cb-49f7-93f5-14003c07788b' },
|
||||
{ Title: 'Mock View Three', Id: '5fda7245-c4a7-403b-adc1-8bd8b481b4ee' }
|
||||
]
|
||||
};
|
||||
|
||||
resolve(listData);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './SPViewPickerService';
|
||||
export * from './ISPViewPickerService';
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "e9f638cb-e59d-4c85-8301-70649469f626",
|
||||
"alias": "AdaptiveCardViewerWebPart",
|
||||
"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": "Adaptive Card Viewer" },
|
||||
"description": { "default": "Adaptive Card Viewer web part" },
|
||||
"iconImageUrl": "data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8'?%3E %3Csvg width='96px' height='96px' viewBox='0 0 96 96' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E %3C!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --%3E %3Ctitle%3Eadaptive_cards%3C/title%3E %3Cdesc%3ECreated with Sketch.%3C/desc%3E %3Cdefs%3E%3C/defs%3E %3Cg id='assets' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E %3Cg id='adaptive_cards'%3E %3Cg id='Group-21'%3E %3Crect id='Rectangle-17-Copy-22' fill='%233A96DD' x='0' y='0' width='96' height='96' rx='48'%3E%3C/rect%3E %3Cg id='Page-1' transform='translate(22.000000, 30.000000)' fill='%23FFFFFF'%3E %3Cg id='Group-3'%3E %3Cpath d='M6.38596491,0.911322807 C2.86091228,0.911322807 0,3.77223509 0,7.29728772 L0,19.9702351 L3.19298246,19.9702351 L3.19298246,7.29728772 C3.19298246,5.53795439 4.62663158,4.10430526 6.38596491,4.10430526 L35.122807,4.10430526 L35.122807,0.911322807 L6.38596491,0.911322807 Z' id='Fill-1'%3E%3C/path%3E %3C/g%3E %3Cpolygon id='Fill-4' points='0 36.0341298 12.922 36.0341298 12.922 32.8411474 5.45042105 32.8411474 15.8052632 22.4894982 13.5478246 20.2320596 3.19298246 30.5837088 3.19298246 23.1632175 0 23.1632175'%3E%3C/polygon%3E %3Cg id='Group-8' transform='translate(35.122807, 0.000000)'%3E %3Cpolygon id='Fill-6' points='3.19298246 0.911322807 3.19298246 4.10430526 10.5144912 4.10430526 0.159649123 14.4591474 2.41708772 16.716586 12.7719298 6.36174386 12.7719298 13.6832526 15.9649123 13.6832526 15.9649123 0.911322807'%3E%3C/polygon%3E %3C/g%3E %3Cpath d='M47.8947368,29.6481649 C47.8947368,31.4106912 46.4610877,32.8411474 44.7017544,32.8411474 L16.1149825,32.8411474 L16.1149825,36.0341298 L44.7017544,36.0341298 C48.226807,36.0341298 51.0877193,33.1732175 51.0877193,29.6481649 L51.0877193,16.8762351 L47.8947368,16.8762351 L47.8947368,29.6481649 Z' id='Fill-9'%3E%3C/path%3E %3C/g%3E %3C/g%3E %3C/g%3E %3C/g%3E %3C/svg%3E",
|
||||
"properties": {
|
||||
"description": "Adaptive Card Viewer"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
|
||||
// Used for property pane
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneChoiceGroup,
|
||||
PropertyPaneToggle,
|
||||
PropertyPaneTextField,
|
||||
IPropertyPaneField
|
||||
} from '@microsoft/sp-property-pane';
|
||||
|
||||
// Used to display help on the property pane
|
||||
import { PropertyPaneWebPartInformation } from '@pnp/spfx-property-controls/lib/PropertyPaneWebPartInformation';
|
||||
|
||||
// Used to select which list
|
||||
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
|
||||
|
||||
// Used to pick which view you want
|
||||
import { PropertyFieldViewPicker, PropertyFieldViewPickerOrderBy } from '../../controls/PropertyFieldViewPicker';
|
||||
|
||||
// Used by the code editor fields
|
||||
import { PropertyFieldCodeEditorLanguages, IPropertyFieldCodeEditorPropsInternal } from '@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor';
|
||||
|
||||
// Used to adapt to changing section background
|
||||
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
import * as strings from 'AdaptiveCardViewerWebPartStrings';
|
||||
import { RootComponent } from './components/RootComponent';
|
||||
import { IAdaptiveCardViewerWebPartProps } from './IAdaptiveCardViewerWebPartProps';
|
||||
import { IAdaptiveCardViewerProps } from './components/IAdaptiveCardViewerProps';
|
||||
|
||||
|
||||
/**
|
||||
* This component is a thin wrapper around the function component.
|
||||
* The job of this class is property management and bootstrapping the component tree
|
||||
*/
|
||||
export default class AdaptiveCardViewerWebPart extends BaseClientSideWebPart <IAdaptiveCardViewerWebPartProps> {
|
||||
private _templatePropertyPaneHelper: IPropertyPaneField<IPropertyFieldCodeEditorPropsInternal>;
|
||||
private _dataPropertyPaneHelper: IPropertyPaneField<IPropertyFieldCodeEditorPropsInternal>;
|
||||
|
||||
protected async onInit(): Promise<void> {
|
||||
await super.onInit();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IAdaptiveCardViewerProps> = React.createElement(
|
||||
RootComponent,
|
||||
{
|
||||
spContext: this.context,
|
||||
spEventObserver: this,
|
||||
acViewerState: null,
|
||||
acViewerStateDispatch: null,
|
||||
...this.properties
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of always loading the property field code editor every time the web part is loaded,
|
||||
* we load it dynamically only when we need to display the property pane.
|
||||
*
|
||||
*/
|
||||
protected async loadPropertyPaneResources(): Promise<void> {
|
||||
// load the property field code editor asynchronously
|
||||
const codeEditor = await import(
|
||||
'@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor'
|
||||
);
|
||||
|
||||
// create a helper for templates
|
||||
this._templatePropertyPaneHelper = codeEditor.PropertyFieldCodeEditor('template', {
|
||||
label: strings.TemplateFieldLabel,
|
||||
panelTitle: strings.TemplateCodeEditorPanelTitle,
|
||||
initialValue: this.properties.template,
|
||||
onPropertyChange: this.onPropertyPaneFieldChanged,
|
||||
properties: this.properties,
|
||||
disabled: false,
|
||||
key: 'codeEditorTemplateId',
|
||||
language: PropertyFieldCodeEditorLanguages.JSON
|
||||
});
|
||||
|
||||
// create a helper for data
|
||||
this._dataPropertyPaneHelper = codeEditor.PropertyFieldCodeEditor('data', {
|
||||
label: strings.DataJSONFieldLabel,
|
||||
panelTitle: strings.DataPanelTitle,
|
||||
key: "dataJSON",
|
||||
initialValue: this.properties.data,
|
||||
onPropertyChange: this.onPropertyPaneFieldChanged,
|
||||
properties: this.properties,
|
||||
disabled: false,
|
||||
language: PropertyFieldCodeEditorLanguages.JSON
|
||||
});
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
const isTemplateJSONBound: boolean = this.properties.templateSource === 'json';
|
||||
const isTemplateUrlBound: boolean = this.properties.templateSource === 'url';
|
||||
|
||||
const isDataJSONBound: boolean = this.properties.useTemplating === true && this.properties.dataSource === 'json';
|
||||
const isDataListBound: boolean = this.properties.useTemplating === true && this.properties.dataSource === 'list';
|
||||
const isDataUrlBound: boolean = this.properties.useTemplating === true && this.properties.dataSource === 'url';
|
||||
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
// Primary group is used to provide the address to show on the map
|
||||
// in a text field in the web part properties
|
||||
groupName: strings.TemplatingGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneWebPartInformation({
|
||||
description: strings.TemplateDescription,
|
||||
moreInfoLink: strings.TemplateMoreInfoUrl,
|
||||
moreInfoLinkTarget: "_blank",
|
||||
key: 'adaptiveCardJSONId'
|
||||
}),
|
||||
PropertyPaneChoiceGroup('templateSource', {
|
||||
label: strings.TemplateSourceFieldLabel,
|
||||
options: [
|
||||
{
|
||||
key: 'json',
|
||||
text: strings.TemplateSourceFieldChoiceJSON,
|
||||
iconProps: {
|
||||
officeFabricIconFontName: 'Code'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
text: strings.TemplateSourceFieldChoiceUrl,
|
||||
iconProps: {
|
||||
officeFabricIconFontName: 'Globe'
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
isTemplateJSONBound && this._templatePropertyPaneHelper,
|
||||
isTemplateUrlBound && PropertyPaneTextField('templateUrl', {
|
||||
label: strings.TemplateUrlLabel,
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: strings.AdaptiveCardTemplatingGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneWebPartInformation({
|
||||
description: strings.AdaptiveCardTemplatingInfoLabel,
|
||||
moreInfoLink: strings.AdaptiveCardTemplatingMoreInfoLinkUrl,
|
||||
moreInfoLinkTarget: "_blank",
|
||||
key: 'adaptiveTemplatingId'
|
||||
}),
|
||||
PropertyPaneToggle('useTemplating', {
|
||||
label: strings.UseAdaptiveTemplatingLabel,
|
||||
checked: this.properties.useTemplating === true
|
||||
}),
|
||||
|
||||
this.properties.useTemplating === true && PropertyPaneChoiceGroup('dataSource', {
|
||||
label: strings.DataSourceFieldLabel,
|
||||
options: [
|
||||
{
|
||||
key: 'json',
|
||||
text: strings.DataSourceFieldChoiceJSON,
|
||||
iconProps: {
|
||||
officeFabricIconFontName: 'Code'
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
text: strings.DataSourceFieldChoiceList,
|
||||
iconProps: {
|
||||
officeFabricIconFontName: 'CustomList'
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
text: strings.DataSourceFieldChoiceUrl,
|
||||
iconProps: {
|
||||
officeFabricIconFontName: 'Globe'
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
isDataJSONBound && this._dataPropertyPaneHelper,
|
||||
isDataJSONBound && PropertyPaneWebPartInformation({
|
||||
description: strings.UseTemplatingDescription,
|
||||
key: 'dataInfoId'
|
||||
}),
|
||||
isDataListBound && PropertyFieldListPicker('list', {
|
||||
label: strings.ListFieldLabel,
|
||||
selectedList: this.properties.list,
|
||||
includeHidden: false,
|
||||
orderBy: PropertyFieldListPickerOrderBy.Title,
|
||||
disabled: false,
|
||||
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
|
||||
properties: this.properties,
|
||||
context: this.context,
|
||||
onGetErrorMessage: null,
|
||||
deferredValidationTime: 0,
|
||||
key: 'listPickerFieldId'
|
||||
}),
|
||||
isDataListBound && PropertyFieldViewPicker('view', {
|
||||
label: strings.ViewFieldLabel,
|
||||
context: this.context,
|
||||
selectedView: this.properties.view,
|
||||
listId: this.properties.list,
|
||||
disabled: false,
|
||||
orderBy: PropertyFieldViewPickerOrderBy.Title,
|
||||
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
|
||||
properties: this.properties,
|
||||
onGetErrorMessage: null,
|
||||
deferredValidationTime: 0,
|
||||
key: 'viewPickerFieldId'
|
||||
}),
|
||||
isDataUrlBound && PropertyPaneTextField('dataUrl', {
|
||||
label: strings.DataUrlLabel,
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
export type TemplateSourceType = 'json' | 'url';
|
||||
export type DataSourceType = 'list' | 'json' | 'url';
|
||||
|
||||
export interface IAdaptiveCardViewerWebPartProps {
|
||||
/**
|
||||
* Either 'json' or 'url'
|
||||
*/
|
||||
templateSource: TemplateSourceType;
|
||||
|
||||
/**
|
||||
* The JSON Adaptive Cards template
|
||||
*/
|
||||
template: string;
|
||||
|
||||
/**
|
||||
* The URL to the template json
|
||||
*/
|
||||
templateUrl: string;
|
||||
|
||||
/**
|
||||
* The static JSON data, if using
|
||||
*/
|
||||
data: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether we'll use adaptive templating or not
|
||||
*/
|
||||
useTemplating: boolean;
|
||||
|
||||
/**
|
||||
* Either 'list' or 'json' or 'url'
|
||||
*/
|
||||
dataSource: DataSourceType;
|
||||
|
||||
/**
|
||||
* The list id of the selected list
|
||||
*/
|
||||
list: string | undefined;
|
||||
|
||||
/**
|
||||
* The view id of the selected view
|
||||
*/
|
||||
view: string | undefined;
|
||||
|
||||
/**
|
||||
* The url of the remote data
|
||||
*/
|
||||
dataUrl: string | undefined;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.adaptiveCardViewer {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react';
|
||||
|
||||
import { AppContext } from '../../../services/AppContext';
|
||||
import { AdaptiveCardHost } from '../../../controls/AdaptiveCardHost/AdaptiveCardHost';
|
||||
|
||||
export const AdaptiveCardViewer: React.FunctionComponent = () => {
|
||||
|
||||
// get the state object from context
|
||||
const { acViewerState } = React.useContext(AppContext);
|
||||
|
||||
|
||||
const [showSpinner, setShowSpinner] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(`AdaptiveCardViewer.useEffect[acViewerState]`);
|
||||
|
||||
if (acViewerState.isLoading) {
|
||||
setShowSpinner(true);
|
||||
} else {
|
||||
setShowSpinner(false);
|
||||
}
|
||||
|
||||
}, [acViewerState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSpinner &&
|
||||
<Spinner styles={{ root: { padding: "2*5px 5px", textAlign: "center" } }} size={SpinnerSize.large} label="Loading..." ariaLive="assertive" />
|
||||
}
|
||||
|
||||
{!showSpinner &&
|
||||
<AdaptiveCardHost themeVariant={acViewerState.themeVariant}
|
||||
template={acViewerState.templateJSON}
|
||||
data={acViewerState.dataJSON}
|
||||
useTemplating={acViewerState.useTemplating} />
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { IAppContextProps } from "../../../services/AppContext";
|
||||
import { IAdaptiveCardViewerWebPartProps } from "../IAdaptiveCardViewerWebPartProps";
|
||||
|
||||
export interface IAdaptiveCardViewerProps extends IAppContextProps, IAdaptiveCardViewerWebPartProps {
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Used to adapt to changing section background
|
||||
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
export type AdaptiveCardViewerStateAction =
|
||||
| { type: 'status', isLoading: boolean, isError: boolean, status?: { message: string, diagnostics: any } }
|
||||
| { type: 'template', payload: any }
|
||||
| { type: 'data', payload: any }
|
||||
| { type: 'theme', payload: IReadonlyTheme | undefined }
|
||||
;
|
||||
|
||||
export interface IAdaptiveCardViewerState {
|
||||
themeVariant: IReadonlyTheme | undefined;
|
||||
templateJSON: string;
|
||||
dataJSON: string;
|
||||
useTemplating: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const reducer: React.Reducer<IAdaptiveCardViewerState, AdaptiveCardViewerStateAction> = (state, action): IAdaptiveCardViewerState => {
|
||||
console.log(`acViewerStateDispatch: ${action.type}`);
|
||||
|
||||
switch (action.type) {
|
||||
case "status":
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.isLoading
|
||||
};
|
||||
case "template":
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
templateJSON: action.payload
|
||||
};
|
||||
case "data":
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
dataJSON: action.payload
|
||||
};
|
||||
case "theme":
|
||||
return {
|
||||
...state,
|
||||
themeVariant: action.payload
|
||||
};
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,153 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { ThemeProvider, ThemeChangedEventArgs } from '@microsoft/sp-component-base';
|
||||
|
||||
import { AppContext, IAppContextProps } from '../../../services/AppContext';
|
||||
import { useCardService } from '../../../services/CardService/CardService';
|
||||
import { useSPListDataService } from '../../../services/SPListDataService/SPListDataService';
|
||||
|
||||
import { IAdaptiveCardViewerProps } from './IAdaptiveCardViewerProps';
|
||||
import { AdaptiveCardViewer } from './AdaptiveCardViewer';
|
||||
import * as AdaptiveCardViewerState from './IAdaptiveCardViewerState';
|
||||
|
||||
/**
|
||||
* This component manages state
|
||||
*/
|
||||
export const RootComponent: React.FunctionComponent<IAdaptiveCardViewerProps> = (props) => {
|
||||
console.log("RootComponent");
|
||||
|
||||
const { cardServiceState, getAdaptiveCardJSON, getDataJSON } = useCardService(props.spContext);
|
||||
const { listServiceState, getListItems } = useSPListDataService(props.spContext);
|
||||
|
||||
// local state to trigger http calls
|
||||
const [templateUrl, setTemplateUrl] = React.useState<string>(undefined);
|
||||
const [dataUrl, setDataUrl] = React.useState<string>(undefined);
|
||||
|
||||
const initialViewerState: AdaptiveCardViewerState.IAdaptiveCardViewerState = {
|
||||
themeVariant: null,
|
||||
templateJSON: props.template,
|
||||
dataJSON: props.data,
|
||||
useTemplating: props.useTemplating,
|
||||
isLoading: false
|
||||
};
|
||||
|
||||
|
||||
// reducer to manage state for this and children
|
||||
const [viewerState, viewerStateDispatch] = React.useReducer(AdaptiveCardViewerState.reducer, initialViewerState);
|
||||
|
||||
//
|
||||
// useEffect => componentDidMount
|
||||
//
|
||||
React.useEffect(() => {
|
||||
console.log("RootComponent.useEffect[]");
|
||||
|
||||
// Register a handler to be notified if the theme variant changes
|
||||
const _handleThemeChangedEvent = (args: ThemeChangedEventArgs) => {
|
||||
viewerStateDispatch({ type: "theme", payload: args.theme });
|
||||
};
|
||||
|
||||
// If it exists, get the theme variant
|
||||
const _themeProvider = props.spContext.serviceScope.consume(ThemeProvider.serviceKey);
|
||||
const _themeVariant = _themeProvider.tryGetTheme();
|
||||
viewerStateDispatch({ type: "theme", payload: _themeVariant });
|
||||
|
||||
_themeProvider.themeChangedEvent.add(props.spEventObserver, _handleThemeChangedEvent);
|
||||
|
||||
return () => {
|
||||
// Cleanup => componentWillUnmount
|
||||
_themeProvider.themeChangedEvent.remove(props.spEventObserver, _handleThemeChangedEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("RootComponent.useEffect[props]");
|
||||
|
||||
/*
|
||||
* (The web part class does not have state. Values set in property pane are passed as props.
|
||||
* On each reload, inspect the props and initialize state accordingly.)
|
||||
*/
|
||||
if (props.templateSource === "json") {
|
||||
viewerStateDispatch({type: "template", payload: props.template});
|
||||
}
|
||||
|
||||
if (props.templateSource === "url" && props.templateUrl) {
|
||||
setTemplateUrl(props.templateUrl);
|
||||
}
|
||||
|
||||
if (props.dataSource === "list" && props.list) {
|
||||
getListItems(props.list, props.view);
|
||||
}
|
||||
|
||||
if (props.dataSource === "url" && props.dataUrl) {
|
||||
setDataUrl(props.dataUrl);
|
||||
}
|
||||
|
||||
if (props.dataSource === "json" && props.data) {
|
||||
viewerStateDispatch({type: "data", payload: props.data});
|
||||
}
|
||||
|
||||
}, [props]);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(`RootComponent.useEffect[cardServiceState] - type: ${cardServiceState.type}`);
|
||||
|
||||
if (cardServiceState.type === "status") {
|
||||
viewerStateDispatch({ type: "status", ...cardServiceState});
|
||||
}
|
||||
|
||||
if (cardServiceState.type === "complete") {
|
||||
if (cardServiceState.dataType === "template") {
|
||||
viewerStateDispatch({ type: "template", payload: cardServiceState.data });
|
||||
}
|
||||
if (cardServiceState.dataType === "data") {
|
||||
viewerStateDispatch({ type: "data", payload: cardServiceState.data });
|
||||
}
|
||||
}
|
||||
}, [cardServiceState]);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("RootComponent.useEffect[templateUrl]");
|
||||
if (templateUrl) {
|
||||
getAdaptiveCardJSON(templateUrl);
|
||||
}
|
||||
}, [templateUrl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("RootComponent.useEffect[dataUrl]");
|
||||
if (dataUrl) {
|
||||
getDataJSON(dataUrl);
|
||||
}
|
||||
}, [dataUrl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(`RootComponent.useEffect[listServiceState] - type: ${listServiceState.type}`);
|
||||
|
||||
if (listServiceState.type === "status") {
|
||||
viewerStateDispatch({ type: "status", ...listServiceState });
|
||||
}
|
||||
|
||||
if (listServiceState.type === "complete") {
|
||||
if (listServiceState.dataType === "template") {
|
||||
viewerStateDispatch({ type: "template", payload: listServiceState.data });
|
||||
}
|
||||
if (listServiceState.dataType === "data") {
|
||||
viewerStateDispatch({ type: "data", payload: listServiceState.data });
|
||||
}
|
||||
}
|
||||
|
||||
}, [listServiceState]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={
|
||||
{
|
||||
spContext: props.spContext,
|
||||
spEventObserver: props.spEventObserver,
|
||||
acViewerState: viewerState,
|
||||
acViewerStateDispatch: viewerStateDispatch
|
||||
}
|
||||
}>
|
||||
<AdaptiveCardViewer />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
39
samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/en-us.js
vendored
Normal file
39
samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
define([], function () {
|
||||
return {
|
||||
PropertyPaneDescription: "Use this web part to display dynamic Adaptive Cards.",
|
||||
TemplateDescription: "You can use any Adaptive Card definition at long as it follows the <a href='https://adaptivecards.io/explorer/' target='_blank'>Adaptive Cards schema</a>.",
|
||||
TemplateMoreInfoUrl: "https://adaptivecards.io/",
|
||||
TemplateSourceFieldLabel: "Template source",
|
||||
TemplateSourceFieldChoiceJSON: "JSON",
|
||||
TemplateSourceFieldChoiceUrl: "URL",
|
||||
TemplateFieldLabel: "Template JSON",
|
||||
TemplateUrlLabel: "Template url",
|
||||
AdaptiveCardTemplatingGroupName: "Adaptive Card Templating",
|
||||
AdaptiveCardTemplatingInfoLabel: "Adaptive Card Templating separates the data from the layout in an Adaptive Card. You can design your card once, then populate it with real data at runtime.",
|
||||
AdaptiveCardTemplatingMoreInfoLinkUrl: "https://docs.microsoft.com/en-us/adaptive-cards/templating/",
|
||||
UseAdaptiveTemplatingLabel: "Use Adaptive Card Templating",
|
||||
DataSourceFieldLabel: "Data source",
|
||||
DataSourceFieldChoiceList: "List",
|
||||
DataSourceFieldChoiceJSON: "JSON",
|
||||
DataSourceFieldChoiceUrl: "URL",
|
||||
UseTemplatingDescription: "You can use any valid JSON data structure.",
|
||||
ListFieldLabel: "Select a list",
|
||||
ViewFieldLabel: "Select a view",
|
||||
DataJSONFieldLabel: "Data JSON",
|
||||
DataUrlLabel: "Data source URL",
|
||||
DataPanelTitle: "Edit Data JSON",
|
||||
AdaptiveCardErrorIntro: "Uh oh, something is wrong with your settings.",
|
||||
PlaceholderDescription: "To use this web part, you need to enter your template JSON/URL.",
|
||||
PlaceholderIconText: "Configure Adaptive Card Host",
|
||||
DataNeededButtonLabel: "Configure data source",
|
||||
DataNeededDescription: "When you use Adaptive Card Templating, you need to provide either static JSON data, or from a SharePoint list.",
|
||||
DataNeededIconText: "You need data!",
|
||||
ConfigureButtonLabel: "Configure",
|
||||
AdaptiveTemplatingEnd: " in the property pane, then specify some data to display.",
|
||||
AdaptiveCardWarningPartTwo: " features. You should configure this web part and enable ",
|
||||
AdaptiveTemplatingWarningIntro: "It looks like you're using a template with ",
|
||||
TemplateJsonError: "Invalid template JSON: ",
|
||||
DataError: "'Invalid data JSON: ' ",
|
||||
AdaptiveCardTemplating: "Adaptive Card Templating"
|
||||
}
|
||||
});
|
44
samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/mystrings.d.ts
vendored
Normal file
44
samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
declare interface IAdaptiveCardViewerWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
TemplatingGroupName: string;
|
||||
TemplateDescription: string;
|
||||
TemplateMoreInfoUrl: string;
|
||||
TemplateSourceFieldLabel: string;
|
||||
TemplateSourceFieldChoiceJSON: string;
|
||||
TemplateSourceFieldChoiceUrl: string;
|
||||
TemplateFieldLabel: string;
|
||||
TemplateCodeEditorPanelTitle: string;
|
||||
TemplateUrlLabel: string;
|
||||
AdaptiveCardTemplatingGroupName: string;
|
||||
AdaptiveCardTemplatingInfoLabel: string;
|
||||
AdaptiveCardTemplatingMoreInfoLinkUrl: string;
|
||||
UseAdaptiveTemplatingLabel: string;
|
||||
DataSourceFieldLabel: string;
|
||||
DataSourceFieldChoiceJSON: string;
|
||||
DataSourceFieldChoiceList: string;
|
||||
DataSourceFieldChoiceUrl: string;
|
||||
UseTemplatingDescription: string;
|
||||
ViewFieldLabel: string;
|
||||
ListFieldLabel: string;
|
||||
DataJSONFieldLabel: string;
|
||||
DataUrlLabel: string;
|
||||
DataPanelTitle: string;
|
||||
AdaptiveCardErrorIntro: string;
|
||||
PlaceholderIconText: string;
|
||||
PlaceholderDescription: string;
|
||||
DataNeededIconText: string;
|
||||
DataNeededDescription: string;
|
||||
DataNeededButtonLabel: string;
|
||||
ConfigureButtonLabel: string;
|
||||
AdaptiveTemplatingWarningIntro: string;
|
||||
AdaptiveCardTemplating: string;
|
||||
AdaptiveCardWarningPartTwo: string;
|
||||
AdaptiveTemplatingEnd: string;
|
||||
TemplateJsonError: string;
|
||||
DataJsonError: string;
|
||||
}
|
||||
|
||||
declare module 'AdaptiveCardViewerWebPartStrings' {
|
||||
const strings: IAdaptiveCardViewerWebPartStrings;
|
||||
export = strings;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts", "src/webparts/adaptiveCardViewer/components/RootComponent.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue