Merge pull request #1259 from pschaeflein/master

This commit is contained in:
Hugo Bernier 2020-04-27 22:27:45 -04:00 committed by GitHub
commit 7277688364
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 20074 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

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

View File

@ -0,0 +1,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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]
});
}
}

View File

@ -0,0 +1,9 @@
export interface IAdaptiveCardActionResult {
type: string;
title: string;
data?: Object;
url: string;
method?: string;
body?: string;
headers?: Array<any>;
}

View File

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

View File

@ -0,0 +1,3 @@
export interface IAdaptiveCardState {
errors: Array<string>;
}

View File

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

View File

@ -0,0 +1,6 @@
export * from './IValidationError';
export * from './IAdaptiveCardProps';
export * from './IAdaptiveCardState';
export * from './AdaptiveCard';
export * from './IAdaptiveCardActionResult';

View File

@ -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"
/></>
);
}
};

View File

@ -0,0 +1,8 @@
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IAdaptiveCardHostProps {
themeVariant: IReadonlyTheme | undefined;
template: string;
data: string;
useTemplating: boolean;
}

View File

@ -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[];
}

View File

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

View File

@ -0,0 +1,4 @@
export interface ISPView {
Id: string;
Title: string;
}

View File

@ -0,0 +1,9 @@
import { ISPView } from ".";
/**
* Defines a collection of SharePoint list views
*/
export interface ISPViews {
value: ISPView[];
}

View File

@ -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);
}

View File

@ -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>
);
}
}

View File

@ -0,0 +1,6 @@
export * from './PropertyFieldViewPicker';
export * from './IPropertyFieldViewPicker';
export * from './PropertyFieldViewPickerHost';
export * from './IPropertyFieldViewPickerHost';
export * from './ISPView';
export * from './ISPViews';

View File

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

View File

@ -0,0 +1,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);

View File

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

View File

@ -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();
}
};

View File

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

View File

@ -0,0 +1,6 @@
import { ISPViews } from "../../controls/PropertyFieldViewPicker";
export interface ISPViewPickerService {
getViews(): Promise<ISPViews>;
}

View File

@ -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);
});
}
}

View File

@ -0,0 +1,2 @@
export * from './SPViewPickerService';
export * from './ISPViewPickerService';

View File

@ -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"
}
}]
}

View File

@ -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,
})
]
}
]
}
]
};
}
}

View File

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

View File

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

View File

@ -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} />
}
</>
);
};

View File

@ -0,0 +1,6 @@
import { IAppContextProps } from "../../../services/AppContext";
import { IAdaptiveCardViewerWebPartProps } from "../IAdaptiveCardViewerWebPartProps";
export interface IAdaptiveCardViewerProps extends IAppContextProps, IAdaptiveCardViewerWebPartProps {
}

View File

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

View File

@ -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>
);
};

View 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"
}
});

View 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

View File

@ -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"
]
}

View File

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