= 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 (
+
+ {this.props.label && }
+
+
+
+
+ );
+ }
+}
diff --git a/samples/react-adaptivecards/src/controls/PropertyFieldViewPicker/index.ts b/samples/react-adaptivecards/src/controls/PropertyFieldViewPicker/index.ts
new file mode 100644
index 000000000..be5692925
--- /dev/null
+++ b/samples/react-adaptivecards/src/controls/PropertyFieldViewPicker/index.ts
@@ -0,0 +1,6 @@
+export * from './PropertyFieldViewPicker';
+export * from './IPropertyFieldViewPicker';
+export * from './PropertyFieldViewPickerHost';
+export * from './IPropertyFieldViewPickerHost';
+export * from './ISPView';
+export * from './ISPViews';
diff --git a/samples/react-adaptivecards/src/index.ts b/samples/react-adaptivecards/src/index.ts
new file mode 100644
index 000000000..fb81db1e2
--- /dev/null
+++ b/samples/react-adaptivecards/src/index.ts
@@ -0,0 +1 @@
+// A file is required to be in the root of the /src directory by the TypeScript compiler
diff --git a/samples/react-adaptivecards/src/services/SPViewPickerService/ISPViewPickerService.ts b/samples/react-adaptivecards/src/services/SPViewPickerService/ISPViewPickerService.ts
new file mode 100644
index 000000000..9132df235
--- /dev/null
+++ b/samples/react-adaptivecards/src/services/SPViewPickerService/ISPViewPickerService.ts
@@ -0,0 +1,6 @@
+import { ISPViews } from "../../controls/PropertyFieldViewPicker";
+
+export interface ISPViewPickerService {
+ getViews(): Promise;
+}
+
diff --git a/samples/react-adaptivecards/src/services/SPViewPickerService/SPViewPickerService.ts b/samples/react-adaptivecards/src/services/SPViewPickerService/SPViewPickerService.ts
new file mode 100644
index 000000000..2635dfa0c
--- /dev/null
+++ b/samples/react-adaptivecards/src/services/SPViewPickerService/SPViewPickerService.ts
@@ -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 {
+ 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 {
+ return new Promise((resolve) => {
+ const listData: ISPViews = {
+ value:[
+ ]
+ };
+
+ resolve(listData);
+ });
+ }
+ /**
+ * Returns 3 fake SharePoint views for the Mock mode
+ */
+ private getViewsFromMock(): Promise {
+ return new Promise((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);
+ });
+ }
+}
diff --git a/samples/react-adaptivecards/src/services/SPViewPickerService/index.ts b/samples/react-adaptivecards/src/services/SPViewPickerService/index.ts
new file mode 100644
index 000000000..69b4f31c3
--- /dev/null
+++ b/samples/react-adaptivecards/src/services/SPViewPickerService/index.ts
@@ -0,0 +1,2 @@
+export * from './SPViewPickerService';
+export * from './ISPViewPickerService';
diff --git a/samples/react-adaptivecards/src/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.manifest.json b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.manifest.json
new file mode 100644
index 000000000..610856e7d
--- /dev/null
+++ b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.manifest.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
+ "id": "778241d6-2ee8-47c8-857a-df3a3b1f1302",
+ "alias": "AdaptiveCardHostWebPart",
+ "componentType": "WebPart",
+ "supportsThemeVariants": true,
+
+ "version": "*",
+ "manifestVersion": 2,
+
+ "requiresCustomScript": false,
+ "supportedHosts": ["SharePointWebPart"],
+
+ "preconfiguredEntries": [{
+ "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
+ "group": { "default": "Other" },
+ "title": { "default": "Adaptive Card Host" },
+ "description": { "default": "Displays Adaptive Cards within SharePoint" },
+ "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": {
+ "template": "",
+ "data": "",
+ "dynamicTemplate": {},
+ "useTemplating": false
+ }
+ }]
+}
diff --git a/samples/react-adaptivecards/src/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.ts b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.ts
new file mode 100644
index 000000000..1e6b53bf0
--- /dev/null
+++ b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.ts
@@ -0,0 +1,333 @@
+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';
+import { DisplayMode } from '@microsoft/sp-core-library';
+
+// Used for property pane
+import {
+ IPropertyPaneConfiguration,
+ PropertyPaneChoiceGroup,
+ PropertyPaneToggle
+} from '@microsoft/sp-property-pane';
+
+// 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 } from '@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor';
+
+// Used to display help on the property pane
+import { PropertyPaneWebPartInformation } from '@pnp/spfx-property-controls/lib/PropertyPaneWebPartInformation';
+
+// Used to adapt to changing section background
+import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
+
+// Used to retrieve SharePoint items
+import { sp } from '@pnp/sp';
+import '@pnp/sp/webs';
+import '@pnp/sp/lists';
+import "@pnp/sp/views";
+//import '@pnp/sp/items';
+
+// Used for localizations
+import * as strings from 'AdaptiveCardHostWebPartStrings';
+
+// Used to render adaptive cards
+import AdaptiveCardHost from './components/AdaptiveCardHost';
+import { IAdaptiveCardHostProps } from './components/IAdaptiveCardHostProps';
+
+
+export type DataSourceType = 'list' | 'json';
+
+export interface IAdaptiveCardHostWebPartProps {
+ /**
+ * The JSON Adaptive Cards template
+ */
+ template: string;
+
+ /**
+ * The static JSON data, if using
+ */
+ data: string | undefined;
+
+ /**
+ * Whether we'll use adaptive templating or not
+ */
+ useTemplating: boolean;
+
+ /**
+ * Either 'list' or 'json'
+ */
+ dataSource: DataSourceType;
+
+ /**
+ * The list id of the selected list
+ */
+ list: string | undefined;
+
+ /**
+ * The view id of the selected view
+ */
+ view: string | undefined;
+}
+
+export default class AdaptiveCardHostWebPart extends BaseClientSideWebPart {
+ private _themeProvider: ThemeProvider;
+ private _themeVariant: IReadonlyTheme | undefined;
+ private _templatePropertyPaneHelper: any;
+ private _dataPropertyPaneHelper: any;
+ private _dataJSON: string;
+ private _viewSchema: string;
+
+ protected async onInit(): Promise {
+
+ // Consume the new ThemeProvider service
+ this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
+
+ // If it exists, get the theme variant
+ this._themeVariant = this._themeProvider.tryGetTheme();
+
+ // Register a handler to be notified if the theme variant changes
+ this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
+
+ await super.onInit();
+
+ sp.setup({
+ spfxContext: this.context
+ });
+
+ await this._loadDataFromList();
+ }
+
+ public async render(): Promise {
+ const { template } = this.properties;
+ const dataJson: string = this.properties.dataSource === 'list' && this.properties.list && this.properties.view ? this._dataJSON : this.properties.data;
+
+ // The Adaptive Card control does not care where the template and data are coming from.
+ // Pass a valid template JSON and -- if using -- some data JSON
+ const element: React.ReactElement = React.createElement(
+ AdaptiveCardHost,
+ {
+ themeVariant: this._themeVariant,
+ template: template,
+ data: dataJson,
+ useTemplating: this.properties.useTemplating === true,
+ context: this.context,
+ displayMode: this.displayMode
+ }
+ );
+
+ ReactDom.render(element, this.domElement);
+ }
+
+ protected onDispose(): void {
+ ReactDom.unmountComponentAtNode(this.domElement);
+ }
+
+ protected get dataVersion(): Version {
+ return Version.parse('1.0');
+ }
+
+ /**
+ * 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 {
+ // 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 help 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 isJSONBound: boolean = this.properties.useTemplating === true && this.properties.dataSource === 'json';
+ const isListBound: boolean = this.properties.useTemplating === true && this.properties.dataSource === 'list';
+
+ 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'
+ }),
+ this._templatePropertyPaneHelper
+ ]
+ },
+ {
+ 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'
+ },
+ }
+ ]
+ }),
+ isJSONBound && this._dataPropertyPaneHelper,
+ isJSONBound && PropertyPaneWebPartInformation({
+ description: strings.UseTemplatingDescription,
+ key: 'dataInfoId'
+ }),
+ isListBound && 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'
+ }),
+ isListBound && PropertyFieldViewPicker('view', {
+ label: strings.ViewPropertyFieldLabel,
+ 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'
+ })
+ ]
+ }
+ ]
+ }
+ ]
+ };
+ }
+
+ /**
+ * Gets called when a property is changed in the property pane.
+ * @param propertyPath The property name that's being changed
+ * @param _oldValue Unused. The old value.
+ * @param _newValue Unused. The new value.
+ *
+ * We use this to force a reload of the data
+ */
+ protected async onPropertyPaneFieldChanged(propertyPath: string, _oldValue: any, _newValue: any): Promise {
+ // If we changed the view or the list or the JSON file
+ // we need to get the view again, and re-load the data
+ if (propertyPath === 'view' || propertyPath === 'list' || propertyPath === 'dataSource') {
+ // Clear the view schema cache
+ this._viewSchema = undefined;
+
+ // Load the data
+ await this._loadDataFromList();
+
+ // Render the card
+ this.render();
+ }
+ }
+
+ /**
+ * Update the current theme variant reference and re-render.
+ *
+ * @PARAM args The new theme
+ */
+ private _handleThemeChangedEvent = (args: ThemeChangedEventArgs) => {
+ this._themeVariant = args.theme;
+ this.render();
+ }
+
+ /**
+ * Loads data from a list by using a cached view
+ */
+ private async _loadDataFromList(): Promise {
+
+ // There is no need to load data from a list if the list and the view aren't configured
+ if (this.properties.dataSource !== 'list' || !this.properties.list || !this.properties.view) {
+ return;
+ }
+
+ // Get the list
+ const list = await sp.web.lists.getById(this.properties.list);
+
+ // If we didn't yet load the view schema, do so now
+ if (!this._viewSchema) {
+ const view = await list.getView(this.properties.view)();
+ this._viewSchema = view.HtmlSchemaXml;
+ }
+
+ // Get the data as returned by the view
+ const { Row: data } = await list.renderListDataAsStream({
+ ViewXml: this._viewSchema
+ });
+
+ // Store that data for later
+ this._dataJSON = JSON.stringify(data);
+
+ if (this.displayMode === DisplayMode.Edit) {
+ console.log("Data JSON", this._dataJSON);
+ }
+ }
+}
diff --git a/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/AdaptiveCardHost.module.scss b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/AdaptiveCardHost.module.scss
new file mode 100644
index 000000000..32637920d
--- /dev/null
+++ b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/AdaptiveCardHost.module.scss
@@ -0,0 +1,5 @@
+@import '~office-ui-fabric-react/dist/sass/References.scss';
+
+.adaptiveCardHost {
+ color: inherit;
+}
diff --git a/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/AdaptiveCardHost.tsx b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/AdaptiveCardHost.tsx
new file mode 100644
index 000000000..8c8d07dbc
--- /dev/null
+++ b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/AdaptiveCardHost.tsx
@@ -0,0 +1,107 @@
+import * as React from 'react';
+import styles from './AdaptiveCardHost.module.scss';
+import { IAdaptiveCardHostProps } from './IAdaptiveCardHostProps';
+
+// Needed for the placeholder when the web part is not configured
+import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
+
+// Needed for displaying adaptive card results
+import { AdaptiveCard, IAdaptiveCardActionResult } from '../../../controls/AdaptiveCard';
+
+// Needed for displaying warnings
+import { MessageBar, MessageBarType, MessageBarButton } from 'office-ui-fabric-react';
+
+// Localization
+import * as strings from 'AdaptiveCardHostWebPartStrings';
+
+export default class AdaptiveCardHost extends React.Component {
+
+ /**
+ * Renders the adaptive card, or one of the many warnings
+ */
+ public render(): React.ReactElement {
+ const {
+ template,
+ data,
+ useTemplating,
+ themeVariant } = this.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 (
+
+ );
+ } else if (needsData) {
+ // If we didn't specify data and we need it, display a different placeholder
+ return (
+
+ );
+ }
+ else {
+ // Display the Adaptive Card
+ return (
+ <>
+ {dataEnabledTemplate && !useTemplating &&
+ {strings.ConfigureButtonLabel}
+
+ }
+ >
+ {strings.AdaptingTemplatingWarningIntro}{strings.AdaptiveCardTemplating}{strings.AdaptiveCardWarningPartTwo}{strings.UseAdaptiveTemplatingLabel}{strings.AdaptiveTemplatingEnd}
+ }
+ >);
+ }
+ }
+
+ /**
+ * Demonstrates how we can respond to actions
+ */
+ private _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 */
+ private _configureHandler = () => {
+ this.props.context.propertyPane.open();
+ }
+}
diff --git a/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/IAdaptiveCardHostProps.ts b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/IAdaptiveCardHostProps.ts
new file mode 100644
index 000000000..59e7aa562
--- /dev/null
+++ b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/components/IAdaptiveCardHostProps.ts
@@ -0,0 +1,12 @@
+import { DisplayMode } from '@microsoft/sp-core-library';
+import { WebPartContext } from "@microsoft/sp-webpart-base";
+import { IReadonlyTheme } from '@microsoft/sp-component-base';
+
+export interface IAdaptiveCardHostProps {
+ themeVariant: IReadonlyTheme | undefined;
+ template: string;
+ data: string;
+ useTemplating: boolean;
+ displayMode: DisplayMode;
+ context: WebPartContext;
+}
diff --git a/samples/react-adaptivecards/src/webparts/adaptiveCardHost/loc/en-us.js b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/loc/en-us.js
new file mode 100644
index 000000000..eedecfd7f
--- /dev/null
+++ b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/loc/en-us.js
@@ -0,0 +1,38 @@
+define([], function () {
+ return {
+ ConfigureButtonLabel: "Configure",
+ AdaptiveTemplatingEnd: " in the property pane, then specify some data to display.",
+ AdaptiveCardWarningPartTwo: " features. You should configure this web part and enable ",
+ AdaptingTemplatingWarningIntro: "It looks like you're using a template with ",
+ 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!",
+ ListFieldLabel: "Select a list",
+ DataSourceFieldChoiceList: "List",
+ DataSourceFieldChoiceJSON: "JSON",
+ DataSourceFieldLabel: "Data source",
+ ViewPropertyFieldLabel: "Select a view",
+ TemplateJsonError: "Invalid template JSON: ",
+ DataError: "'Invalid data JSON: ' ",
+ TemplatingJsonError: "Invalid Adaptive Card Templating JSON: ",
+ DataPanelTitle: "Edit Data JSON",
+ BadPropertyIdError: "Bad property id",
+ AdaptiveCardErrorIntro: "Uh oh, something is wrong with your settings.",
+ TemplateMoreInfoUrl: "https://adaptivecards.io/",
+ TemplateDescription: "You can use any Adaptive Card definition at long as it follows the Adaptive Cards schema.",
+ TemplateCodeEditorPanelTitle: "Edit Template JSON",
+ AdaptiveCardTemplatingGroupName: "Adaptive Card Templating",
+ UseTemplatingDescription: "You can use any valid JSON data structure.",
+ PlaceholderButtonLabel: "Configure",
+ PlaceholderDescription: "To use this web part, you need to enter your template JSON.",
+ PlaceholderIconText: "Configure Adaptive Card Host",
+ AdaptiveCardTemplatingMoreInfoLinkUrl: "https://docs.microsoft.com/en-us/adaptive-cards/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.",
+ DataJSONFieldLabel: "Data JSON",
+ UseAdaptiveTemplatingLabel: "Use Adaptive Card Templating",
+ PropertyPaneDescription: "Use this web part to display dynamic Adaptive Cards.",
+ BasicGroupName: "Adaptive Cards",
+ TemplateFieldLabel: "Template JSON",
+ AdaptiveCardTemplating: "Adaptive Card Templating"
+ }
+});
diff --git a/samples/react-adaptivecards/src/webparts/adaptiveCardHost/loc/mystrings.d.ts b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/loc/mystrings.d.ts
new file mode 100644
index 000000000..52f1dc448
--- /dev/null
+++ b/samples/react-adaptivecards/src/webparts/adaptiveCardHost/loc/mystrings.d.ts
@@ -0,0 +1,41 @@
+declare interface IAdaptiveCardHostWebPartStrings {
+ ConfigureButtonLabel: string;
+ AdaptiveTemplatingEnd: string;
+ AdaptiveCardWarningPartTwo: string;
+ AdaptingTemplatingWarningIntro: string;
+ DataNeededButtonLabel: string;
+ DataNeededDescription: string;
+ DataNeededIconText: string;
+ ListFieldLabel: string;
+ DataSourceFieldChoiceList: string;
+ DataSourceFieldChoiceJSON: string;
+ DataSourceFieldLabel: string;
+ ViewPropertyFieldLabel: string;
+ TemplateJsonError: string;
+ DataJsonError: string;
+ TemplatingJsonError: string;
+ DataPanelTitle: string;
+ BadPropertyIdError: string;
+ AdaptiveCardErrorIntro: string;
+ TemplateMoreInfoUrl: string;
+ TemplateDescription: string;
+ TemplateCodeEditorPanelTitle: string;
+ AdaptiveCardTemplatingGroupName: string;
+ UseTemplatingDescription: string;
+ PlaceholderButtonLabel: string;
+ PlaceholderDescription: string;
+ PlaceholderIconText: string;
+ AdaptiveCardTemplatingMoreInfoLinkUrl: string;
+ AdaptiveCardTemplatingInfoLabel: string;
+ DataJSONFieldLabel: string;
+ UseAdaptiveTemplatingLabel: string;
+ PropertyPaneDescription: string;
+ TemplatingGroupName: string;
+ TemplateFieldLabel: string;
+ AdaptiveCardTemplating: string;
+}
+
+declare module 'AdaptiveCardHostWebPartStrings' {
+ const strings: IAdaptiveCardHostWebPartStrings;
+ export = strings;
+}
diff --git a/samples/react-adaptivecards/teams/0ef02257-af33-4a55-9760-d3107a3640b8_color.png b/samples/react-adaptivecards/teams/0ef02257-af33-4a55-9760-d3107a3640b8_color.png
new file mode 100644
index 000000000..a8d279707
Binary files /dev/null and b/samples/react-adaptivecards/teams/0ef02257-af33-4a55-9760-d3107a3640b8_color.png differ
diff --git a/samples/react-adaptivecards/teams/0ef02257-af33-4a55-9760-d3107a3640b8_outline.png b/samples/react-adaptivecards/teams/0ef02257-af33-4a55-9760-d3107a3640b8_outline.png
new file mode 100644
index 000000000..6df4a038d
Binary files /dev/null and b/samples/react-adaptivecards/teams/0ef02257-af33-4a55-9760-d3107a3640b8_outline.png differ
diff --git a/samples/react-adaptivecards/teams/778241d6-2ee8-47c8-857a-df3a3b1f1302_color.png b/samples/react-adaptivecards/teams/778241d6-2ee8-47c8-857a-df3a3b1f1302_color.png
new file mode 100644
index 000000000..a8d279707
Binary files /dev/null and b/samples/react-adaptivecards/teams/778241d6-2ee8-47c8-857a-df3a3b1f1302_color.png differ
diff --git a/samples/react-adaptivecards/teams/778241d6-2ee8-47c8-857a-df3a3b1f1302_outline.png b/samples/react-adaptivecards/teams/778241d6-2ee8-47c8-857a-df3a3b1f1302_outline.png
new file mode 100644
index 000000000..6df4a038d
Binary files /dev/null and b/samples/react-adaptivecards/teams/778241d6-2ee8-47c8-857a-df3a3b1f1302_outline.png differ
diff --git a/samples/react-adaptivecards/tools/pre-version.js b/samples/react-adaptivecards/tools/pre-version.js
new file mode 100644
index 000000000..c1373dd6d
--- /dev/null
+++ b/samples/react-adaptivecards/tools/pre-version.js
@@ -0,0 +1,64 @@
+/**
+ * This script updates the package-solution version analogue to the
+ * the package.json file.
+ */
+
+if (process.env.npm_package_version === undefined) {
+
+ throw 'Package version cannot be evaluated';
+
+}
+
+// define path to package-solution file
+const solution = './config/package-solution.json',
+ teams = './teams/manifest.json';
+
+// require filesystem instance
+const fs = require('fs');
+
+// get next automated package version from process variable
+const nextPkgVersion = process.env.npm_package_version;
+
+// make sure next build version match
+const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
+ nextPkgVersion : nextPkgVersion.split('-')[0];
+
+// Update version in SPFx package-solution if exists
+if (fs.existsSync(solution)) {
+
+ // read package-solution file
+ const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
+ // parse file as json
+ const solutionContents = JSON.parse(solutionFileContent);
+
+ // set property of version to next version
+ solutionContents.solution.version = nextVersion + '.0';
+
+ // save file
+ fs.writeFileSync(
+ solution,
+ // convert file back to proper json
+ JSON.stringify(solutionContents, null, 2),
+ 'UTF-8');
+
+}
+
+// Update version in teams manifest if exists
+if (fs.existsSync(teams)) {
+
+ // read package-solution file
+ const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
+ // parse file as json
+ const teamsContent = JSON.parse(teamsManifestContent);
+
+ // set property of version to next version
+ teamsContent.version = nextVersion;
+
+ // save file
+ fs.writeFileSync(
+ teams,
+ // convert file back to proper json
+ JSON.stringify(teamsContent, null, 2),
+ 'UTF-8');
+
+}
diff --git a/samples/react-adaptivecards/tsconfig.json b/samples/react-adaptivecards/tsconfig.json
new file mode 100644
index 000000000..ae5dbdfee
--- /dev/null
+++ b/samples/react-adaptivecards/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "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"
+ ],
+ "esModuleInterop": true
+ },
+ "include": [
+ "src/**/*.ts", "src/controls/AdaptiveCard/AdaptiveCard.tsx"
+ ],
+ "exclude": [
+ "node_modules",
+ "lib"
+ ]
+}
diff --git a/samples/react-adaptivecards/tslint.json b/samples/react-adaptivecards/tslint.json
new file mode 100644
index 000000000..23fa2aa43
--- /dev/null
+++ b/samples/react-adaptivecards/tslint.json
@@ -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
+ }
+}
\ No newline at end of file