= 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-hooks/src/controls/PropertyFieldViewPicker/index.ts b/samples/react-adaptivecards-hooks/src/controls/PropertyFieldViewPicker/index.ts
new file mode 100644
index 000000000..be5692925
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/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-hooks/src/index.ts b/samples/react-adaptivecards-hooks/src/index.ts
new file mode 100644
index 000000000..fb81db1e2
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/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-hooks/src/services/AppContext.ts b/samples/react-adaptivecards-hooks/src/services/AppContext.ts
new file mode 100644
index 000000000..d0a481d1a
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/services/AppContext.ts
@@ -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;
+}
+
+export const AppContext = createContext(undefined);
+
+export const getContentPackManagerState = () => useContext(AppContext);
diff --git a/samples/react-adaptivecards-hooks/src/services/CardService/CardService.ts b/samples/react-adaptivecards-hooks/src/services/CardService/CardService.ts
new file mode 100644
index 000000000..966703e8a
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/services/CardService/CardService.ts
@@ -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};
+
+};
diff --git a/samples/react-adaptivecards-hooks/src/services/CardService/CardServiceReducer.ts b/samples/react-adaptivecards-hooks/src/services/CardService/CardServiceReducer.ts
new file mode 100644
index 000000000..50813b968
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/services/CardService/CardServiceReducer.ts
@@ -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 = (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();
+ }
+};
diff --git a/samples/react-adaptivecards-hooks/src/services/SPListDataService/SPListDataService.ts b/samples/react-adaptivecards-hooks/src/services/SPListDataService/SPListDataService.ts
new file mode 100644
index 000000000..a69f8de8d
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/services/SPListDataService/SPListDataService.ts
@@ -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 };
+};
diff --git a/samples/react-adaptivecards-hooks/src/services/SPViewPickerService/ISPViewPickerService.ts b/samples/react-adaptivecards-hooks/src/services/SPViewPickerService/ISPViewPickerService.ts
new file mode 100644
index 000000000..9132df235
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/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-hooks/src/services/SPViewPickerService/SPViewPickerService.ts b/samples/react-adaptivecards-hooks/src/services/SPViewPickerService/SPViewPickerService.ts
new file mode 100644
index 000000000..2635dfa0c
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/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-hooks/src/services/SPViewPickerService/index.ts b/samples/react-adaptivecards-hooks/src/services/SPViewPickerService/index.ts
new file mode 100644
index 000000000..69b4f31c3
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/services/SPViewPickerService/index.ts
@@ -0,0 +1,2 @@
+export * from './SPViewPickerService';
+export * from './ISPViewPickerService';
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.manifest.json b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.manifest.json
new file mode 100644
index 000000000..7e27b24c4
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.manifest.json
@@ -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"
+ }
+ }]
+}
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.ts b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.ts
new file mode 100644
index 000000000..e7881f88d
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/AdaptiveCardViewerWebPart.ts
@@ -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 {
+ private _templatePropertyPaneHelper: IPropertyPaneField;
+ private _dataPropertyPaneHelper: IPropertyPaneField;
+
+ protected async onInit(): Promise {
+ await super.onInit();
+ }
+
+ public render(): void {
+ const element: React.ReactElement = 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 {
+ // 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,
+ })
+ ]
+ }
+ ]
+ }
+ ]
+ };
+ }
+}
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/IAdaptiveCardViewerWebPartProps.ts b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/IAdaptiveCardViewerWebPartProps.ts
new file mode 100644
index 000000000..562022b8a
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/IAdaptiveCardViewerWebPartProps.ts
@@ -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;
+}
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/AdaptiveCardViewer.module.scss b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/AdaptiveCardViewer.module.scss
new file mode 100644
index 000000000..9847e8177
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/AdaptiveCardViewer.module.scss
@@ -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;
+ }
+ }
+}
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/AdaptiveCardViewer.tsx b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/AdaptiveCardViewer.tsx
new file mode 100644
index 000000000..6e5d09636
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/AdaptiveCardViewer.tsx
@@ -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 &&
+
+ }
+
+ {!showSpinner &&
+
+ }
+ >
+ );
+};
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/IAdaptiveCardViewerProps.ts b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/IAdaptiveCardViewerProps.ts
new file mode 100644
index 000000000..c08870abb
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/IAdaptiveCardViewerProps.ts
@@ -0,0 +1,6 @@
+import { IAppContextProps } from "../../../services/AppContext";
+import { IAdaptiveCardViewerWebPartProps } from "../IAdaptiveCardViewerWebPartProps";
+
+export interface IAdaptiveCardViewerProps extends IAppContextProps, IAdaptiveCardViewerWebPartProps {
+}
+
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/IAdaptiveCardViewerState.ts b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/IAdaptiveCardViewerState.ts
new file mode 100644
index 000000000..0543bffaa
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/IAdaptiveCardViewerState.ts
@@ -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 = (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;
+ }
+};
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/RootComponent.tsx b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/RootComponent.tsx
new file mode 100644
index 000000000..942285bff
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/components/RootComponent.tsx
@@ -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 = (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(undefined);
+ const [dataUrl, setDataUrl] = React.useState(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 (
+
+
+
+ );
+};
+
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/en-us.js b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/en-us.js
new file mode 100644
index 000000000..0dba3af65
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/en-us.js
@@ -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 Adaptive Cards schema.",
+ 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"
+ }
+});
diff --git a/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/mystrings.d.ts b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/mystrings.d.ts
new file mode 100644
index 000000000..f87fc0b29
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/src/webparts/adaptiveCardViewer/loc/mystrings.d.ts
@@ -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;
+}
diff --git a/samples/react-adaptivecards-hooks/teams/e9f638cb-e59d-4c85-8301-70649469f626_color.png b/samples/react-adaptivecards-hooks/teams/e9f638cb-e59d-4c85-8301-70649469f626_color.png
new file mode 100644
index 000000000..a8d279707
Binary files /dev/null and b/samples/react-adaptivecards-hooks/teams/e9f638cb-e59d-4c85-8301-70649469f626_color.png differ
diff --git a/samples/react-adaptivecards-hooks/teams/e9f638cb-e59d-4c85-8301-70649469f626_outline.png b/samples/react-adaptivecards-hooks/teams/e9f638cb-e59d-4c85-8301-70649469f626_outline.png
new file mode 100644
index 000000000..6df4a038d
Binary files /dev/null and b/samples/react-adaptivecards-hooks/teams/e9f638cb-e59d-4c85-8301-70649469f626_outline.png differ
diff --git a/samples/react-adaptivecards-hooks/tsconfig.json b/samples/react-adaptivecards-hooks/tsconfig.json
new file mode 100644
index 000000000..b4d144d3c
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/tsconfig.json
@@ -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"
+ ]
+}
diff --git a/samples/react-adaptivecards-hooks/tslint.json b/samples/react-adaptivecards-hooks/tslint.json
new file mode 100644
index 000000000..23fa2aa43
--- /dev/null
+++ b/samples/react-adaptivecards-hooks/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