Skip to content

Commit 2d12d13

Browse files
feat: card component (microsoft#355)
1 parent b22398b commit 2d12d13

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+20522
-1
lines changed

.github/workflows/create-release.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
`${process.env.GITHUB_WORKSPACE}/AutoWidthLabel/AutoWidthLabel`,
5151
`${process.env.GITHUB_WORKSPACE}/Breadcrumb/Breadcrumb`,
5252
`${process.env.GITHUB_WORKSPACE}/Calendar/Calendar`,
53+
`${process.env.GITHUB_WORKSPACE}/Card/Card`,
5354
`${process.env.GITHUB_WORKSPACE}/CommandBar/CommandBar`,
5455
`${process.env.GITHUB_WORKSPACE}/ContextMenu/ContextMenu`,
5556
`${process.env.GITHUB_WORKSPACE}/DetailsList/DetailsList`,
@@ -99,6 +100,12 @@ jobs:
99100
npm install
100101
npm ci
101102
103+
- name: Install Dependencies in Card
104+
run: |
105+
cd ./Card
106+
npm install
107+
npm ci
108+
102109
- name: Install Dependencies in CommandBar
103110
run: |
104111
cd ./CommandBar

.github/workflows/pr_validate_all.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
- "./AutoWidthLabel"
1616
- "./Breadcrumb"
1717
- "./Calendar"
18+
- "./Card"
1819
- "./CommandBar"
1920
- "./ContextMenu"
2021
- "./DetailsList"

Card/.eslintrc.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"parser": "@typescript-eslint/parser",
3+
"env": {
4+
"browser": true,
5+
"commonjs": true,
6+
"es6": true,
7+
"jest": true,
8+
"jasmine": true
9+
},
10+
"extends": [
11+
"plugin:@typescript-eslint/recommended",
12+
"plugin:prettier/recommended",
13+
"prettier",
14+
"plugin:sonarjs/recommended"
15+
],
16+
"parserOptions": {
17+
"project": "./tsconfig.json"
18+
},
19+
"plugins": [
20+
"@typescript-eslint",
21+
"prettier",
22+
"react-hooks",
23+
"react",
24+
"sonarjs"
25+
],
26+
"rules": {
27+
"prettier/prettier": "error"
28+
},
29+
"overrides": [
30+
{
31+
"files": [
32+
"*.ts"
33+
],
34+
"rules": {
35+
"camelcase": [
36+
2,
37+
{
38+
"properties": "never"
39+
}
40+
]
41+
}
42+
}
43+
],
44+
"ignorePatterns": [
45+
"**/generated/**",
46+
"**/.eslint*.json"
47+
]
48+
}

Card/.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
6+
# generated directory
7+
**/generated
8+
9+
# output directory
10+
/out
11+
/coverage
12+
# msbuild output directories
13+
/bin
14+
/obj
15+
16+
# MSBuild Binary and Structured Log
17+
*.binlog

Card/.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"semi": true,
3+
"trailingComma": "all",
4+
"singleQuote": false,
5+
"printWidth": 120,
6+
"tabWidth": 2,
7+
"endOfLine":"auto"
8+
}

Card/Card.pcfproj

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<PropertyGroup>
4+
<PowerAppsTargetsPath>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps</PowerAppsTargetsPath>
5+
</PropertyGroup>
6+
7+
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
8+
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.props" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.props')" />
9+
10+
<PropertyGroup>
11+
<Name>Card</Name>
12+
<ProjectGuid>fab62631-997f-45d7-afb8-d8b512cfb4fd</ProjectGuid>
13+
<OutputPath>$(MSBuildThisFileDirectory)out\controls</OutputPath>
14+
<PcfBuildMode>production</PcfBuildMode>
15+
</PropertyGroup>
16+
17+
<PropertyGroup>
18+
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
19+
<!--Remove TargetFramework when this is available in 16.1-->
20+
<TargetFramework>net462</TargetFramework>
21+
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
22+
</PropertyGroup>
23+
24+
<ItemGroup>
25+
<PackageReference Include="Microsoft.PowerApps.MSBuild.Pcf" Version="1.*" />
26+
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
27+
</ItemGroup>
28+
29+
<ItemGroup>
30+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\.gitignore" />
31+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\bin\**" />
32+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\obj\**" />
33+
<ExcludeDirectories Include="$(OutputPath)\**" />
34+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.pcfproj" />
35+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.pcfproj.user" />
36+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.sln" />
37+
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\node_modules\**" />
38+
</ItemGroup>
39+
40+
<ItemGroup>
41+
<None Include="$(MSBuildThisFileDirectory)\**" Exclude="@(ExcludeDirectories)" />
42+
</ItemGroup>
43+
44+
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />
45+
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.targets" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.targets')" />
46+
47+
</Project>

Card/Card/Card.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as React from "react";
2+
import { IInputs, IOutputs } from "./generated/ManifestTypes";
3+
import { CardCanvas } from "./components/CardCanvas";
4+
import { CustomCardProps } from "./components/Component.types";
5+
import { ManifestPropertyNames, Orientation, Size, StringConstants } from "./ManifestConstants";
6+
import { getItemsFromDataset } from "./components/Toolbar/datasetmapping";
7+
import { IToolbarItem } from "./components/Toolbar/Component.types";
8+
import { getUrlfromImage } from "./components/helper";
9+
import { ContextEx } from "./components/ContextExtended";
10+
11+
export class Card implements ComponentFramework.ReactControl<IInputs, IOutputs> {
12+
context: ComponentFramework.Context<IInputs>;
13+
items: IToolbarItem[];
14+
componentKey = "powerapps-corecontrol-toolbar";
15+
height?: number;
16+
notifyOutputChanged: () => void;
17+
/**
18+
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
19+
* Data-set values are not initialized here, use updateView.
20+
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
21+
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
22+
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
23+
*/
24+
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void): void {
25+
this.notifyOutputChanged = notifyOutputChanged;
26+
this.context = context;
27+
this.onSelect = this.onSelect.bind(this);
28+
context.mode.trackContainerResize(true);
29+
}
30+
31+
/**
32+
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
33+
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
34+
* @returns ReactElement root react element for the control
35+
*/
36+
public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
37+
this.context = context;
38+
const datasetChanged = context.updatedProperties.indexOf(ManifestPropertyNames.dataset) > -1 || !this.items;
39+
const allocatedWidth = parseInt(context.mode.allocatedWidth as unknown as string);
40+
const allocatedHeight = parseInt(context.mode.allocatedHeight as unknown as string);
41+
if (datasetChanged) {
42+
this.items = getItemsFromDataset(context.parameters.Items);
43+
// When the items or layout change,
44+
// re-render the toolbar to remeasure, i.e. shrink/grow accordingly
45+
this.componentKey = this.componentKey.concat("_1");
46+
}
47+
48+
const props = {
49+
key: this.componentKey,
50+
items: this.items.filter((i) => i.visible).slice(0, 2),
51+
width: allocatedWidth,
52+
height: allocatedHeight,
53+
onSelected: this.onSelect,
54+
disabled: context.mode.isControlDisabled || (context as unknown as ContextEx).mode.isRead,
55+
title: context.parameters.Title.raw,
56+
subTitle: context.parameters.Subtitle.raw,
57+
headerImage: getUrlfromImage(context.parameters.HeaderImage.raw),
58+
visible: context.mode.isVisible,
59+
size: Size[context.parameters.Size.raw],
60+
onResize: this.onResize,
61+
ariaLabel: context.parameters.AccessibleLabel?.raw ?? "",
62+
orientation: Orientation[context.parameters.Alignment.raw],
63+
placePreview: context.parameters.ImagePlacement?.raw ?? StringConstants.AboveHeader,
64+
accessibleLabel: context.parameters.AccessibleLabel.raw ?? "",
65+
image: getUrlfromImage(context.parameters.Image.raw),
66+
description: context.parameters.Description.raw,
67+
getPopoverRoot: this.getPopoverRoot,
68+
} as CustomCardProps;
69+
70+
return React.createElement(CardCanvas, props);
71+
}
72+
73+
private onResize = (height: number): void => {
74+
this.height = height;
75+
this.notifyOutputChanged();
76+
};
77+
78+
private onSelect = (item?: IToolbarItem): void => {
79+
if (item && item.data) {
80+
this.context.parameters.Items.openDatasetItem(item.data.getNamedReference());
81+
} else if (this.items && this.items.length > 0) {
82+
this.context.parameters.Items.openDatasetItem(this.items[0].data.getNamedReference());
83+
} else {
84+
this.context.events.OnSelect();
85+
}
86+
this.notifyOutputChanged();
87+
};
88+
89+
/**
90+
* It is called by the framework prior to a control receiving new data.
91+
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as "bound" or "output"
92+
*/
93+
public getOutputs(): IOutputs {
94+
return { AutoHeight: this.height || parseInt(this.context.mode.allocatedHeight as unknown as string) };
95+
}
96+
97+
/**
98+
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
99+
* i.e. cancelling any pending remote calls, removing listeners, etc.
100+
*/
101+
public destroy(): void {
102+
// Add code to cleanup control if necessary
103+
}
104+
105+
private getPopoverRoot(): HTMLElement {
106+
// if we can't find the target layer, we fallback to the fluent provider, then finally <body>
107+
// note: the ID below should always match the one set in Studio's `ThemeWrapperHelper.tsx`
108+
const root =
109+
document.querySelector("#__fluentv9popover__") || document.querySelector(".fui-FluentProvider") || document.body;
110+
return root as HTMLElement;
111+
}
112+
}

Card/Card/ControlManifest.Input.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<manifest>
3+
<control namespace="PowerCAT" constructor="Card" version="0.0.40" display-name-key="Card_Display_Key" description-key="Card_Description_Key" control-type="virtual" >
4+
<property name="AccessibleLabel" display-name-key="Card_AccessibleLabel_Display_Key" description-key="Card_AccessibleLabel_Desc_Key" of-type="SingleLine.Text" usage="input" required="false"/>
5+
<property name="Title" display-name-key="Card_Title_Display_Key" of-type="SingleLine.Text" usage="bound" required="false" default-value="Title"/>
6+
<property name="Subtitle" display-name-key="Card_Subtitle_Display_Key" of-type="Multiple" usage="bound" required="false" default-value="Subtitle"/>
7+
<property name="Description" display-name-key="Card_Description_Display_Key" of-type="Multiple" usage="bound" required="false"/>
8+
<property name="HeaderImage" display-name-key="Card_HeaderImage_Display_Key" of-type="Image" usage="input" required="false" pfx-default-value="SampleImage"/>
9+
<property name="Image" display-name-key="Card_Image_Display_Key" of-type="Image" usage="input" required="false"/>
10+
<property name="ImagePlacement" display-name-key="Card_ImagePlacement_Display_Key" description-key="Card_ImagePlacement_Desc_Key" usage="input" of-type="Enum" default-value="Above header">
11+
<value name="Above header" display-name-key="Card_ImagePlacement_AboveHeader">Above header</value>
12+
<value name="Below header" display-name-key="Card_ImagePlacement_BelowHeader">Below header</value>
13+
</property>
14+
<property name="Size" display-name-key="Card_Size_Display_Key" description-key="Card_Size_Desc_Key" usage="input" of-type="Enum" default-value="Medium">
15+
<value name="Small" display-name-key="Card_Size_Small">Small</value>
16+
<value name="Medium" display-name-key="Card_Size_Medium">Medium</value>
17+
<value name="Large" display-name-key="Card_Size_Large">Large</value>
18+
</property>
19+
<property name="Alignment" display-name-key="Card_Alignment_Display_Key" description-key="Card_Alignment_Desc_Key" of-type="Enum" usage="input" default-value="Vertical">
20+
<value name="Vertical" display-name-key="Card_Alignment_Vertical">Vertical</value>
21+
<value name="Horizontal" display-name-key="Card_Alignment_Horizontal">Horizontal</value>
22+
</property>
23+
<!--AutoHeight property-->
24+
<property name="AutoHeight" display-name-key="Card_AutoHeight_Display_Key" description-key="Card_AutoHeight_Desc_Key" usage="output" of-type="Whole.None" />
25+
<!--Toolbar properties start-->
26+
<data-set name="Items" display-name-key="Card_Toolbar_Items_Display_Key" description-key="Card_Toolbar_Items_Desc_Key" pfx-default-value="Table(
27+
{ItemKey: &quot;mail&quot;, ItemDisplayName: &quot;Contact&quot;, ItemIconName: &quot;Mail&quot;, ItemAppearance: &quot;&quot;, ItemIconStyle: &quot;Regular&quot;, ItemTooltip: &quot;Send mail&quot;, ItemVisible: true, ItemDisabled: false},{ItemKey: &quot;chat&quot;, ItemDisplayName: &quot;Chat&quot;, ItemIconName: &quot;Chat&quot;, ItemAppearance: &quot;&quot;, ItemIconStyle: &quot;Regular&quot;, ItemTooltip: &quot;Chat&quot;})">
28+
<property-set name="ItemDisplayName" display-name-key="Card_Toolbar_Items_ItemDisplayName" of-type="SingleLine.Text" usage="bound" required="true"/>
29+
<property-set name="ItemKey" display-name-key="Card_Toolbar_Items_ItemKey" of-type="SingleLine.Text" usage="bound" required="true"/>
30+
<property-set name="ItemDisabled" display-name-key="Card_Toolbar_Items_ItemDisabled" of-type="TwoOptions" usage="bound" required="false"/>
31+
<property-set name="ItemVisible" display-name-key="Card_Toolbar_Items_ItemVisible" of-type="TwoOptions" usage="bound" required="false"/>
32+
<property-set name="ItemIconName" display-name-key="Card_Toolbar_Items_ItemIconName" of-type="SingleLine.Text" usage="bound" required="false"/>
33+
<property-set name="ItemIconStyle" display-name-key="Card_Toolbar_Items_ItemIconStyle" of-type="SingleLine.Text" usage="bound" required="false"/>
34+
<property-set name="ItemAppearance" display-name-key="Card_Toolbar_Items_ItemAppearance" of-type="SingleLine.Text" usage="bound" required="false"/>
35+
<property-set name="ItemTooltip" display-name-key="Card_Toolbar_Items_ItemTooltip" of-type="SingleLine.Text" usage="bound" required="false"/>
36+
</data-set>
37+
<!--Toolbar properties ends-->
38+
<property name="TabIndex" hidden="true" display-name-key="NA" description-key="NA" of-type="Whole.None" usage="input" required="false"/>
39+
<property name="Tooltip" hidden="true" display-name-key="NA" description-key="NA" of-type="SingleLine.Text" usage="input" required="false"/>
40+
<common-property name="Height" default-value="110" pfx-default-value="Self.AutoHeight"/>
41+
<common-property name="Width" default-value="300"/>
42+
<common-event name="OnSelect" pfx-default-value="/*Switch(Self.Selected.ItemKey,&quot;mail&quot;,Notify(&quot;Contact clicked&quot;),&quot;chat&quot;,Notify(&quot;Chat clicked&quot;), Notify(&quot;Unrecognized button clicked&quot;))*/"/>
43+
<resources>
44+
<code path="index.ts" order="1"/>
45+
<resx path="strings/Card.1033.resx" version="1.0.0"/>
46+
<platform-library name="React" version="16.8.6"/>
47+
<platform-library name="Fluent" version="9.19.1"/>
48+
</resources>
49+
<feature-usage>
50+
<uses-feature name="ExplicitCommonEvents" required="true"/>
51+
</feature-usage>
52+
</control>
53+
</manifest>

Card/Card/ManifestConstants.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export const enum ItemColumns {
2+
DisplayName = "ItemDisplayName",
3+
Key = "ItemKey",
4+
IconName = "ItemIconName",
5+
IconStyle = "ItemIconStyle",
6+
Visible = "ItemVisible",
7+
Disabled = "ItemDisabled",
8+
Appearance = "ItemAppearance",
9+
Tooltip = "ItemTooltip",
10+
}
11+
12+
export const enum StringConstants {
13+
Regular = "Regular",
14+
Subtle = "subtle",
15+
AboveHeader = "Above header",
16+
BelowHeader = "Below header",
17+
}
18+
19+
export const Size = {
20+
Large: "large",
21+
Medium: "medium",
22+
Small: "small",
23+
};
24+
25+
export const Layout = {
26+
"Icon only": "icon",
27+
"Text only": "text",
28+
"Icon before": "before",
29+
"Icon after": "after",
30+
"Icon above": "above",
31+
};
32+
33+
export const enum ManifestPropertyNames {
34+
dataset = "dataset",
35+
}
36+
37+
export const Orientation = {
38+
Vertical: "vertical",
39+
Horizontal: "horizontal",
40+
};
41+
42+
export const enum Alignment {
43+
Alignment = "Alignment",
44+
}

0 commit comments

Comments
 (0)