Merge pull request #1177 from hugoabernier/react-adaptivecards

Wow, you think you could make the `README.md` any longer?
This commit is contained in:
Hugo Bernier 2020-04-02 13:24:31 -04:00 committed by GitHub
commit 848b5a8d49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 22695 additions and 0 deletions

View File

@ -0,0 +1,328 @@
{
"always-semicolon": true,
"color-case": "lower",
"block-indent": "\t",
"color-shorthand": true,
"element-case": "lower",
"leading-zero": false,
"quotes": "double",
"sort-order-fallback": "abc",
"space-before-colon": "",
"space-after-colon": " ",
"space-before-combinator": " ",
"space-after-combinator": " ",
"space-between-declarations": "\n",
"space-before-opening-brace": "",
"space-after-opening-brace": "\n",
"space-after-selector-delimiter": "\n",
"space-before-selector-delimiter": "",
"space-before-closing-brace": "\n",
"strip-spaces": true,
"tab-size": true,
"unitless-zero": true,
"vendor-prefix-align": true,
"sort-order": [
[
"$extend",
"$include"
],
[
"font",
"font-family",
"font-size",
"font-weight",
"font-style",
"font-variant",
"font-size-adjust",
"font-stretch",
"font-effect",
"font-emphasize",
"font-emphasize-position",
"font-emphasize-style",
"font-smooth",
"line-height"
],
[
"position",
"z-index",
"top",
"right",
"bottom",
"left"
],
[
"display",
"visibility",
"float",
"clear",
"overflow",
"overflow-x",
"overflow-y",
"-ms-overflow-x",
"-ms-overflow-y",
"clip",
"zoom",
"flex-direction",
"flex-order",
"flex-pack",
"flex-align"
],
[
"-webkit-box-sizing",
"-moz-box-sizing",
"box-sizing",
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left"
],
[
"table-layout",
"empty-cells",
"caption-side",
"border-spacing",
"border-collapse",
"list-style",
"list-style-position",
"list-style-type",
"list-style-image"
],
[
"content",
"quotes",
"counter-reset",
"counter-increment",
"resize",
"cursor",
"-webkit-user-select",
"-moz-user-select",
"-ms-user-select",
"user-select",
"nav-index",
"nav-up",
"nav-right",
"nav-down",
"nav-left",
"-webkit-transition",
"-moz-transition",
"-ms-transition",
"-o-transition",
"transition",
"-webkit-transition-delay",
"-moz-transition-delay",
"-ms-transition-delay",
"-o-transition-delay",
"transition-delay",
"-webkit-transition-timing-function",
"-moz-transition-timing-function",
"-ms-transition-timing-function",
"-o-transition-timing-function",
"transition-timing-function",
"-webkit-transition-duration",
"-moz-transition-duration",
"-ms-transition-duration",
"-o-transition-duration",
"transition-duration",
"-webkit-transition-property",
"-moz-transition-property",
"-ms-transition-property",
"-o-transition-property",
"transition-property",
"-webkit-transform",
"-moz-transform",
"-ms-transform",
"-o-transform",
"transform",
"-webkit-transform-origin",
"-moz-transform-origin",
"-ms-transform-origin",
"-o-transform-origin",
"transform-origin",
"-webkit-animation",
"-moz-animation",
"-ms-animation",
"-o-animation",
"animation",
"-webkit-animation-name",
"-moz-animation-name",
"-ms-animation-name",
"-o-animation-name",
"animation-name",
"-webkit-animation-duration",
"-moz-animation-duration",
"-ms-animation-duration",
"-o-animation-duration",
"animation-duration",
"-webkit-animation-play-state",
"-moz-animation-play-state",
"-ms-animation-play-state",
"-o-animation-play-state",
"animation-play-state",
"-webkit-animation-timing-function",
"-moz-animation-timing-function",
"-ms-animation-timing-function",
"-o-animation-timing-function",
"animation-timing-function",
"-webkit-animation-delay",
"-moz-animation-delay",
"-ms-animation-delay",
"-o-animation-delay",
"animation-delay",
"-webkit-animation-iteration-count",
"-moz-animation-iteration-count",
"-ms-animation-iteration-count",
"-o-animation-iteration-count",
"animation-iteration-count",
"-webkit-animation-direction",
"-moz-animation-direction",
"-ms-animation-direction",
"-o-animation-direction",
"animation-direction",
"text-align",
"-webkit-text-align-last",
"-moz-text-align-last",
"-ms-text-align-last",
"text-align-last",
"vertical-align",
"white-space",
"text-decoration",
"text-emphasis",
"text-emphasis-color",
"text-emphasis-style",
"text-emphasis-position",
"text-indent",
"-ms-text-justify",
"text-justify",
"letter-spacing",
"word-spacing",
"-ms-writing-mode",
"text-outline",
"text-transform",
"text-wrap",
"text-overflow",
"-ms-text-overflow",
"text-overflow-ellipsis",
"text-overflow-mode",
"-ms-word-wrap",
"word-wrap",
"word-break",
"-ms-word-break",
"-moz-tab-size",
"-o-tab-size",
"tab-size",
"-webkit-hyphens",
"-moz-hyphens",
"hyphens",
"pointer-events"
],
[
"opacity",
"filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
"-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
"-ms-interpolation-mode",
"color",
"border",
"border-width",
"border-style",
"border-color",
"border-top",
"border-top-width",
"border-top-style",
"border-top-color",
"border-right",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color",
"-webkit-border-radius",
"-moz-border-radius",
"border-radius",
"-webkit-border-top-left-radius",
"-moz-border-radius-topleft",
"border-top-left-radius",
"-webkit-border-top-right-radius",
"-moz-border-radius-topright",
"border-top-right-radius",
"-webkit-border-bottom-right-radius",
"-moz-border-radius-bottomright",
"border-bottom-right-radius",
"-webkit-border-bottom-left-radius",
"-moz-border-radius-bottomleft",
"border-bottom-left-radius",
"-webkit-border-image",
"-moz-border-image",
"-o-border-image",
"border-image",
"-webkit-border-image-source",
"-moz-border-image-source",
"-o-border-image-source",
"border-image-source",
"-webkit-border-image-slice",
"-moz-border-image-slice",
"-o-border-image-slice",
"border-image-slice",
"-webkit-border-image-width",
"-moz-border-image-width",
"-o-border-image-width",
"border-image-width",
"-webkit-border-image-outset",
"-moz-border-image-outset",
"-o-border-image-outset",
"border-image-outset",
"-webkit-border-image-repeat",
"-moz-border-image-repeat",
"-o-border-image-repeat",
"border-image-repeat",
"outline",
"outline-width",
"outline-style",
"outline-color",
"outline-offset",
"background",
"filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
"background-color",
"background-image",
"background-repeat",
"background-attachment",
"background-position",
"background-position-x",
"-ms-background-position-x",
"background-position-y",
"-ms-background-position-y",
"-webkit-background-clip",
"-moz-background-clip",
"background-clip",
"background-origin",
"-webkit-background-size",
"-moz-background-size",
"-o-background-size",
"background-size",
"box-decoration-break",
"-webkit-box-shadow",
"-moz-box-shadow",
"box-shadow",
"filter:progid:DXImageTransform.Microsoft.gradient",
"-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
"text-shadow"
]
]
}

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

32
samples/react-adaptivecards/.gitignore vendored Normal file
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,10 @@
{
"extends": "stylelint-config-standard",
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true
}
}

View File

@ -0,0 +1,29 @@
{
"@pnp/generator-spfx": {
"framework": "react",
"pnpFramework": "reactjs.plus",
"pnp-libraries": [
"@pnp/spfx-property-controls",
"rush@3.3"
],
"pnp-ci": "no-devops",
"pnp-vetting": [
"webpack-analyzer",
"stylelint",
"csscomb"
],
"spfxenv": "spo",
"pnp-testing": []
},
"@microsoft/generator-sharepoint": {
"environment": "spo",
"framework": "react",
"isCreatingSolution": false,
"version": "1.10.0",
"libraryName": "adaptive-card-host",
"libraryId": "ce1e3712-fb7a-4564-b2d3-c81e45936afd",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,449 @@
# Adaptive Cards Host
## Summary
[Adaptive Cards](https://adaptivecards.io/) are platform-agnostic snippets of user interface, written in JSON, that apps and services can openly exchange.
You'll find Adaptive Cards in many Microsoft products such as Microsoft Bots Framework, Cortana Skills, Outlook Actionable Messages, Power Automate, Power Virtual Agents, Microsoft Teams, Windows Timeline, and so on.
This sample creates an Adaptive Cards Host web part that you can use to display Adaptive Cards in your SharePoint applications.
![Adaptive Cards Host](./assets/AdaptiveCardsHostAllSamples.gif)
## 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)
## Prerequisites
There are no pre-requisites, but you'll probably want to have some [Adaptive Cards samples](https://adaptivecards.io/samples/) ready to try out the web part.
## Solution
Solution|Author(s)
--------|---------
react-adaptivecards | Hugo Bernier ([Tahoe Ninjas](https://tahoeninjas.blog), [@bernier](https://twitter.com/bernierh))
## Version history
Version|Date|Comments
-------|----|--------
1.0|March 24, 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`
## Features
The sample also supports the following features and concepts:
### Adaptive Cards
This web part supports all features of the [Adaptive Cards schema](https://adaptivecards.io/explorer), including the **Preview** Adaptive Card Templating features (see below).
The web part uses a brand new control called `AdaptiveCard`, which you can use to embed Adaptive Cards in your own solutions.
![Web Part Sample](./assets/AdaptiveCardsHost.gif)
To use the web part, follow these simple steps:
1. Insert the **Adaptive Cards Host** web part on your page
2. When prompted to **Configure Adaptive Card Host**, click **Configure** to display the property pane.
3. Select **Template JSON** code editor and paste your Adaptive Card template JSON. You can find template JSON samples on the Adaptive Cards [samples and templates page](https://adaptivecards.io/samples/). Alternatively, you can copy the following JSON:
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Create Adaptive Card web parts",
"weight": "bolder",
"size": "medium"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "Image",
"url": "https://pbs.twimg.com/profile_images/1112818195797176322/jAUChJDM_400x400.jpg",
"size": "small",
"style": "person"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Hugo Bernier",
"weight": "bolder",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "none",
"text": "Created {{DATE(2020-03-24T06:08:39Z, SHORT)}}",
"isSubtle": true,
"wrap": true
}
]
}
]
},
{
"type": "TextBlock",
"text": "Now that we have a control to embed Adaptive Cards in SharePoint web parts, we need to create some cool web parts.",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "Board:",
"value": "Web Part Samples"
},
{
"title": "List:",
"value": "Backlog"
},
{
"title": "Assigned to:",
"value": "Hugo Bernier"
},
{
"title": "Due date:",
"value": "Not set"
}
]
}
],
"actions": [
{
"type": "Action.ShowCard",
"title": "Set due date",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Date",
"id": "dueDate"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "OK"
}
]
}
},
{
"type": "Action.ShowCard",
"title": "Comment",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"id": "comment",
"isMultiline": true,
"placeholder": "Enter your comment"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "OK"
}
]
}
}
]
}
```
### Adaptive Card Templating
[Adaptive Card Templating](https://docs.microsoft.com/en-us/adaptive-cards/templating/) is a new feature that allows you to separate your Adaptive Card data from its layout.
The web part allows you to **Use Adaptive Card Templating** features with a simple toggle.
![Templating toggle](./assets/Templating.gif)
When you use **Adaptive Card Templating**, the web part will prompt you to enter two things:
* Your Adaptive Card template JSON
* Some data -- either as a JSON structure or by connecting to a SharePoint list or document library.
![You Need Data!](./assets/YouNeedData.png)
When browsing the [Adaptive Cards samples and templates](https://adaptivecards.io/samples/) page, you can enable Adaptive Card Templating by toggling the **Use Adaptive Card Templating** setting on the page.
To use an Adaptive Card Templating sample, copy the **Template JSON** to the web part's **Template JSON**, and the **Data JSON** to the **Data JSON** web part property.
When you import a template JSON with Adaptive Card templating syntax, the web part will remind you that you should turn on **Use Adaptive Card Templating**.
![Templating Detection](./assets/TemplatingDetection.png)
Using Adaptive Card Templating, you can connect your Adaptive Card to live data from a SharePoint list or document library.
To do so, follow these steps:
1. In your SharePoint list or document library, create a view and define the columns you want to display, as well as your filter, sort, and limit options. The Adaptive Card will use the same settings.
2. Add the **Adaptive Card Host** web part to a SharePoint page.
3. Edit the web part's property pane. We'll skip the **Template JSON** and go straight to the **Adaptive Card Templating** section.
4. Turn on **Use Adaptive Card Templating**
![Turn on Use Adaptive Card Templating](./assets/EnableTemplating.gif)
5. From the **Data Source**, select **List**
6. From the **Select a list** drop-down, pick your list or document library.
7. From the **Select a view** drop-down, pick the view you created in **Step 1**
![Select library and view](./assets/SelectLibrary.gif)
8. To create your template JSON, you'll need to know what data your web part will receive from your list or library. If you need help with finding out what your data looks like, the web part will log your data JSON to your browser's dev tools console when your page is in **Edit** mode. Just look for **Data JSON** on the **Console** tab and copy that JSON.
![Dev Tools Console](./assets/DevTools.png)
9. Armed with your **Data JSON**, go to the [Adaptive Card Designer](https://adaptivecards.io/designer) and paste your **Data JSON** in the **Sample Data Editor** then select the **Copy the structure of this data into the Data Structure toolbox** button to load your sample data into the designer.
![Sample Data Editor](./assets/SampleDataEditor.png)
10. Create your Adaptive Card template using the editor. For repeating rows of data, make sure to bind your `Container` or `ColumnSet` to the `{$root}` **Data context**. All repeating items within the container/column set should bind to the `{$data}` **Data context**. Make sure to use the **Preview mode** to test your template before continuing.
![Data context](./assets/DataContext.png)
11. Copy your template JSON from the **Card Payload Editor**
12. Back on your SharePoint page, paste your JSON in the web parts **Template JSON**
> NOTE: In a future version of this web part, we'll embed an Adaptive Card editor to make this process easier.
For example, this is a very **basic** template JSON for a SharePoint Document Library**:
```json
{
"type": "AdaptiveCard",
"body": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "50px",
"id": "I",
"items": [
{
"type": "TextBlock",
"text": "ID",
"weight": "Bolder"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "File Name",
"weight": "Bolder"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Editor",
"weight": "Bolder"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Modified",
"weight": "Bolder"
}
]
}
]
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "50px",
"items": [
{
"type": "TextBlock",
"text": "{ID}",
"$data": "{$data}"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "{FileLeafRef}",
"$data": "{$data}"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "{Editor[0].title}",
"$data": "{$data}"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "{Modified}",
"$data": "{$data}"
}
]
}
],
"$data": "{$root}"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
```
> DISCLAIMER: The sample above focuses on simplicity to get you started. We'd recommend you explore [list formatting](https://aka.ms/list-formatting) before using Adaptive Cards to render your lists. However, if you find that list formatting does not meet your needs, try using Adaptive Cards.
### Adaptive Card Actions
The Adaptive Card schema supports [Actions](https://adaptivecards.io/explorer/Action.OpenUrl.html), which allow you to create interactive Adaptive Cards.
The web part supports all Adaptive Card actions, including:
* [`Action.OpenUrl`](https://adaptivecards.io/explorer/Action.OpenUrl.html)
* [`Action.Submit`](https://adaptivecards.io/explorer/Action.Submit.html)
* [`Action.ShowCard`](https://adaptivecards.io/explorer/Action.ShowCard.html)
* [`Action.ToggleVisibility`](https://adaptivecards.io/explorer/Action.ToggleVisibility.html)
However, if you want to support `Action.Submit` in your own web part, we recommend you implement your own `onExecuteAction` event handler on your `AdaptiveCard` control. The sample will simply pop-up an alert instead of performing a submit.
### Markdown Support
Adaptive Cards provide support for Markdown syntax, but they do not render the Markdown.
In our sample, we used [Markdown-it](https://www.npmjs.com/package/markdown-it) to process any Markdown syntax in the rendered Adaptive Card before displaying it in the web part.
### Fallback Support
Adaptive Cards provide a mechanism for handling unknown Adaptive Card elements called **Fallback**.
When the client doesn't support an Adaptive Card element, or the version specified, it will display text (or markdown) by specifying a `fallbackText` element.
The web part provides fallback handling.
![Fallback Support -- Steve Holt!](./assets/ErrorHandling.png)
### Support for UI Fabric
The component that is responsible for rendering Adaptive Cards in every application is called the **Adaptive Card Host**. It is the responsibility of every Adaptive Card Host to render the cards so that they take on the appearance of the application where the Adaptive Card will be displayed.
The following is an example of how the same Adaptive Card renders differently in each Adaptive Card Host:
![Adaptive Card Host Rendering](./assets/AdaptiveCardHostRendering.gif)
When the web part renders Adaptive Cards in SharePoint, it injects [UI Fabric](https://developer.microsoft.com/en-us/fabric#/) styles to ensure that the rendered card blends in with other SharePoint content.
![Support for UI Fabric](./assets/UIFabric.png)
The web part also ensures that the Adaptive Cards resize according to the web part's dimensions and adapt to the site's theme.
![Layouts and colors affect the rendering](./assets/SharePointHost.gif)
### Section Background Color Support
In addition to theme color support, the web part uses the method described at [Supporting Section Background](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/supporting-section-backgrounds) to adjust to changing section colors.
![Changing the section background color changes the Adaptive Card background as well](./assets/BackgroundColors.gif)
Keep in mind that some Adaptive Cards may include images which may not render well on different background colors. Also, the **Strong** section color may cause action buttons to render the same color as the background, but this appears to be a limitation of themes in SharePoint rather than a limitation of Adaptive Cards.
### SharePoint RenderListDataAsStream API
To retrieve your list data, we use PnPJS's [RenderListDataAsStream](https://pnp.github.io/pnpjs/sp/lists/#render-list-data-as-stream) method. `RenderListDataAsStream` is the same method SharePoint calls when rendering lists and document libraries.
The method allows us to pass the selected View's CAML query and retrieve all the relevant data we need in a single call -- including the values for lookup and managed metadata fields.
### Dynamically Loading Property Pane Resources
Our web part uses the [Property Field Code Editor](https://sharepoint.github.io/sp-dev-fx-property-controls/controls/PropertyFieldCodeEditor/) from the [@pnp/spfx-property-controls](https://sharepoint.github.io/) library.
However, as you load more controls in your web part's property pane, you may notice that your web part takes longer to load. That is because the bundle sizes tend to increase with each new library you include in your solution.
This web part uses `loadPropertyResources` to dynamically load some of its property pane controls when the property pane is shown, as described in the Microsoft documentation on [Dynamic loading](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/dynamic-loading#special-property-pane-dynamic-loading).
You can use `loadPropertyResources` in your web part to reduce the number of resources that are loaded when the web part renders, and only load resources when the property pane of your web part is shown.
### Dependent Property Pane Controls
The web part sample conditionally hides and shows property pane controls depending on which settings you choose.
![Dependent Property Pane Controls](./assets/DependentControls.gif)
### View Picker
To create this sample, we had to create our own **View picker** control, which allows you to select a view from the list or document library you selected.
The sample includes the source code for the `PropertyFieldViewPicker`, which renders the control, and `SPViewPickerService` which retrieves the views.
The code for both components is largely derived from the [@pnp/spfx-property-controls](https://sharepoint.github.io/) library's [PropertyFieldListPicker control](https://sharepoint.github.io/sp-dev-fx-property-controls/controls/PropertyFieldListPicker/).
We have submitted a [pull request](https://github.com/SharePoint/sp-dev-fx-property-controls/pull/230) to add the `PropertyFieldViewPicker` control to the @pnp/spfx-property-controls. If the pull request is accepted, we'll update this sample to remove the extraneous code and refer to the version of the @pnp/spfx-property-controls `PropertyFieldViewPicker`.
## For More Information
If you'd like to read more about the various concepts illustrated in this sample, please refer to the following links:
* [Adaptive Cards](https://adaptivecards.io/)
* [Adaptive Card Templating](https://docs.microsoft.com/en-us/adaptive-cards/templating/)
* [Adaptive Cards Designer](https://adaptivecards.io/designer/)
* [Adaptive Cards Viewer for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=tomlm.vscode-adaptivecards)
* [Dynamic Loading of Property Pane Resources](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/dynamic-loading#special-property-pane-dynamic-loading)
* [Supporting Section Background](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/supporting-section-backgrounds)
* [Use cascading dropdowns in web part properties](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/use-cascading-dropdowns-in-web-part-properties)
* [Using the SharePoint RenderListDataAsStream API to fetch lookup and single managed metadata field values](https://www.eliostruyf.com/using-sharepoint-renderlistdataasstream-api-fetch-lookup-single-managed-metadata-field-values/)
* [RenderListDataAsStream](https://pnp.github.io/pnpjs/sp/lists/#render-list-data-as-stream)
* [Introduction to Adaptive Cards](https://poszytek.eu/en/microsoft-en/introduction-to-adaptive-cards/)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-adaptivecards" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

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-host-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.js",
"manifest": "./src/webparts/adaptiveCardHost/AdaptiveCardHostWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"AdaptiveCardHostWebPartStrings": "lib/webparts/adaptiveCardHost/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": "adaptive-card-host",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "adaptive-cards",
"id": "ce1e3712-fb7a-4564-b2d3-c81e45936afd",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/adaptive-cars.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 -->"
}

77
samples/react-adaptivecards/gulpfile.js vendored Normal file
View File

@ -0,0 +1,77 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
// Create clean development package
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Webpack Bundle Anlayzer
* Reference and gulp task
*/
const bundleAnalyzer = require('webpack-bundle-analyzer');
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
const lastDirName = path.basename(__dirname);
const dropPath = path.join(__dirname, 'temp', 'stats');
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
generateStatsFile: true,
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
logLevel: 'error'
}));
return generatedConfiguration;
}
});
/**
* StyleLinter configuration
* Reference and custom gulp task
*/
const stylelint = require('gulp-stylelint');
/* Stylelinter sub task */
let styleLintSubTask = build.subTask('stylelint', (gulp) => {
return gulp
.src('src/**/*.scss')
.pipe(stylelint({
failAfterError: false,
reporters: [{
formatter: 'string',
console: true
}]
}));
});
/* end sub task */
build.rig.addPreBuildTask(styleLintSubTask);
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);

20020
samples/react-adaptivecards/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
{
"name": "adaptive-card-host",
"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",
"preversion": "node ./tools/pre-version.js",
"postversion": "gulp dist"
},
"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.3",
"@pnp/spfx-controls-react": "1.16.0",
"@pnp/spfx-property-controls": "1.16.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.0",
"markdown-it": "^10.0.0",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5",
"save": "^2.4.0"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "1.0.0",
"gulp-stylelint": "^13.0.0",
"stylelint": "^13.0.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.14.2",
"webpack-bundle-analyzer": "^3.6.0"
}
}

View File

@ -0,0 +1,230 @@
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 'AdaptiveCardHostWebPartStrings';
/**
* 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.TemplatingJsonError + 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
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,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,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": "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
}
}]
}

View File

@ -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<IAdaptiveCardHostWebPartProps> {
private _themeProvider: ThemeProvider;
private _themeVariant: IReadonlyTheme | undefined;
private _templatePropertyPaneHelper: any;
private _dataPropertyPaneHelper: any;
private _dataJSON: string;
private _viewSchema: string;
protected async onInit(): Promise<void> {
// 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<void> {
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<IAdaptiveCardHostProps> = 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<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 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<void> {
// 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<void> {
// 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);
}
}
}

View File

@ -0,0 +1,5 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.adaptiveCardHost {
color: inherit;
}

View File

@ -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<IAdaptiveCardHostProps, {}> {
/**
* Renders the adaptive card, or one of the many warnings
*/
public render(): React.ReactElement<IAdaptiveCardHostProps> {
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 (
<Placeholder iconName='Code'
iconText={strings.PlaceholderIconText}
description={strings.PlaceholderDescription}
buttonLabel='Configure'
onConfigure={this._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={this._configureHandler} />
);
}
else {
// Display the Adaptive Card
return (
<>
{dataEnabledTemplate && !useTemplating && <MessageBar
dismissButtonAriaLabel="Close"
messageBarType={MessageBarType.warning}
actions={
<div>
<MessageBarButton onClick={this._configureHandler}>{strings.ConfigureButtonLabel}</MessageBarButton>
</div>
}
>
{strings.AdaptingTemplatingWarningIntro}<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={this._executeActionHandler}
className={styles.adaptiveCardHost}
/></>);
}
}
/**
* 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();
}
}

View File

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

View File

@ -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 <a href='https://adaptivecards.io/explorer/' target='_blank'>Adaptive Cards schema</a>.",
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"
}
});

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

View File

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

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