react-adaptivecards-hooks sample
This commit is contained in:
parent
5823549a5b
commit
c3e0db817d
|
@ -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,14 @@
|
||||||
|
## react-adaptivecards-hooks
|
||||||
|
|
||||||
|
### 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 |
|
|
@ -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