Skip to content

Commit

Permalink
feat: card component (#355)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ramakrishnan24689 authored Nov 8, 2024
1 parent b22398b commit 2d12d13
Show file tree
Hide file tree
Showing 44 changed files with 20,522 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
`${process.env.GITHUB_WORKSPACE}/AutoWidthLabel/AutoWidthLabel`,
`${process.env.GITHUB_WORKSPACE}/Breadcrumb/Breadcrumb`,
`${process.env.GITHUB_WORKSPACE}/Calendar/Calendar`,
`${process.env.GITHUB_WORKSPACE}/Card/Card`,
`${process.env.GITHUB_WORKSPACE}/CommandBar/CommandBar`,
`${process.env.GITHUB_WORKSPACE}/ContextMenu/ContextMenu`,
`${process.env.GITHUB_WORKSPACE}/DetailsList/DetailsList`,
Expand Down Expand Up @@ -99,6 +100,12 @@ jobs:
npm install
npm ci
- name: Install Dependencies in Card
run: |
cd ./Card
npm install
npm ci
- name: Install Dependencies in CommandBar
run: |
cd ./CommandBar
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr_validate_all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
- "./AutoWidthLabel"
- "./Breadcrumb"
- "./Calendar"
- "./Card"
- "./CommandBar"
- "./ContextMenu"
- "./DetailsList"
Expand Down
48 changes: 48 additions & 0 deletions Card/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"parser": "@typescript-eslint/parser",
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"jest": true,
"jasmine": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier",
"plugin:sonarjs/recommended"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint",
"prettier",
"react-hooks",
"react",
"sonarjs"
],
"rules": {
"prettier/prettier": "error"
},
"overrides": [
{
"files": [
"*.ts"
],
"rules": {
"camelcase": [
2,
{
"properties": "never"
}
]
}
}
],
"ignorePatterns": [
"**/generated/**",
"**/.eslint*.json"
]
}
17 changes: 17 additions & 0 deletions Card/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# generated directory
**/generated

# output directory
/out
/coverage
# msbuild output directories
/bin
/obj

# MSBuild Binary and Structured Log
*.binlog
8 changes: 8 additions & 0 deletions Card/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 120,
"tabWidth": 2,
"endOfLine":"auto"
}
47 changes: 47 additions & 0 deletions Card/Card.pcfproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PowerAppsTargetsPath>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps</PowerAppsTargetsPath>
</PropertyGroup>

<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.props" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.props')" />

<PropertyGroup>
<Name>Card</Name>
<ProjectGuid>fab62631-997f-45d7-afb8-d8b512cfb4fd</ProjectGuid>
<OutputPath>$(MSBuildThisFileDirectory)out\controls</OutputPath>
<PcfBuildMode>production</PcfBuildMode>
</PropertyGroup>

<PropertyGroup>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<!--Remove TargetFramework when this is available in 16.1-->
<TargetFramework>net462</TargetFramework>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.PowerApps.MSBuild.Pcf" Version="1.*" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\.gitignore" />
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\bin\**" />
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\obj\**" />
<ExcludeDirectories Include="$(OutputPath)\**" />
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.pcfproj" />
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.pcfproj.user" />
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\*.sln" />
<ExcludeDirectories Include="$(MSBuildThisFileDirectory)\node_modules\**" />
</ItemGroup>

<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)\**" Exclude="@(ExcludeDirectories)" />
</ItemGroup>

<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.targets" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Pcf.targets')" />

</Project>
112 changes: 112 additions & 0 deletions Card/Card/Card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as React from "react";
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { CardCanvas } from "./components/CardCanvas";
import { CustomCardProps } from "./components/Component.types";
import { ManifestPropertyNames, Orientation, Size, StringConstants } from "./ManifestConstants";
import { getItemsFromDataset } from "./components/Toolbar/datasetmapping";
import { IToolbarItem } from "./components/Toolbar/Component.types";
import { getUrlfromImage } from "./components/helper";
import { ContextEx } from "./components/ContextExtended";

export class Card implements ComponentFramework.ReactControl<IInputs, IOutputs> {
context: ComponentFramework.Context<IInputs>;
items: IToolbarItem[];
componentKey = "powerapps-corecontrol-toolbar";
height?: number;
notifyOutputChanged: () => void;
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @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.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @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.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void): void {
this.notifyOutputChanged = notifyOutputChanged;
this.context = context;
this.onSelect = this.onSelect.bind(this);
context.mode.trackContainerResize(true);
}

/**
* 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.
* @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
* @returns ReactElement root react element for the control
*/
public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
this.context = context;
const datasetChanged = context.updatedProperties.indexOf(ManifestPropertyNames.dataset) > -1 || !this.items;
const allocatedWidth = parseInt(context.mode.allocatedWidth as unknown as string);
const allocatedHeight = parseInt(context.mode.allocatedHeight as unknown as string);
if (datasetChanged) {
this.items = getItemsFromDataset(context.parameters.Items);
// When the items or layout change,
// re-render the toolbar to remeasure, i.e. shrink/grow accordingly
this.componentKey = this.componentKey.concat("_1");
}

const props = {
key: this.componentKey,
items: this.items.filter((i) => i.visible).slice(0, 2),
width: allocatedWidth,
height: allocatedHeight,
onSelected: this.onSelect,
disabled: context.mode.isControlDisabled || (context as unknown as ContextEx).mode.isRead,
title: context.parameters.Title.raw,
subTitle: context.parameters.Subtitle.raw,
headerImage: getUrlfromImage(context.parameters.HeaderImage.raw),
visible: context.mode.isVisible,
size: Size[context.parameters.Size.raw],
onResize: this.onResize,
ariaLabel: context.parameters.AccessibleLabel?.raw ?? "",
orientation: Orientation[context.parameters.Alignment.raw],
placePreview: context.parameters.ImagePlacement?.raw ?? StringConstants.AboveHeader,
accessibleLabel: context.parameters.AccessibleLabel.raw ?? "",
image: getUrlfromImage(context.parameters.Image.raw),
description: context.parameters.Description.raw,
getPopoverRoot: this.getPopoverRoot,
} as CustomCardProps;

return React.createElement(CardCanvas, props);
}

private onResize = (height: number): void => {
this.height = height;
this.notifyOutputChanged();
};

private onSelect = (item?: IToolbarItem): void => {
if (item && item.data) {
this.context.parameters.Items.openDatasetItem(item.data.getNamedReference());
} else if (this.items && this.items.length > 0) {
this.context.parameters.Items.openDatasetItem(this.items[0].data.getNamedReference());
} else {
this.context.events.OnSelect();
}
this.notifyOutputChanged();
};

/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as "bound" or "output"
*/
public getOutputs(): IOutputs {
return { AutoHeight: this.height || parseInt(this.context.mode.allocatedHeight as unknown as string) };
}

/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void {
// Add code to cleanup control if necessary
}

private getPopoverRoot(): HTMLElement {
// if we can't find the target layer, we fallback to the fluent provider, then finally <body>
// note: the ID below should always match the one set in Studio's `ThemeWrapperHelper.tsx`
const root =
document.querySelector("#__fluentv9popover__") || document.querySelector(".fui-FluentProvider") || document.body;
return root as HTMLElement;
}
}
53 changes: 53 additions & 0 deletions Card/Card/ControlManifest.Input.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="PowerCAT" constructor="Card" version="0.0.40" display-name-key="Card_Display_Key" description-key="Card_Description_Key" control-type="virtual" >
<property name="AccessibleLabel" display-name-key="Card_AccessibleLabel_Display_Key" description-key="Card_AccessibleLabel_Desc_Key" of-type="SingleLine.Text" usage="input" required="false"/>
<property name="Title" display-name-key="Card_Title_Display_Key" of-type="SingleLine.Text" usage="bound" required="false" default-value="Title"/>
<property name="Subtitle" display-name-key="Card_Subtitle_Display_Key" of-type="Multiple" usage="bound" required="false" default-value="Subtitle"/>
<property name="Description" display-name-key="Card_Description_Display_Key" of-type="Multiple" usage="bound" required="false"/>
<property name="HeaderImage" display-name-key="Card_HeaderImage_Display_Key" of-type="Image" usage="input" required="false" pfx-default-value="SampleImage"/>
<property name="Image" display-name-key="Card_Image_Display_Key" of-type="Image" usage="input" required="false"/>
<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">
<value name="Above header" display-name-key="Card_ImagePlacement_AboveHeader">Above header</value>
<value name="Below header" display-name-key="Card_ImagePlacement_BelowHeader">Below header</value>
</property>
<property name="Size" display-name-key="Card_Size_Display_Key" description-key="Card_Size_Desc_Key" usage="input" of-type="Enum" default-value="Medium">
<value name="Small" display-name-key="Card_Size_Small">Small</value>
<value name="Medium" display-name-key="Card_Size_Medium">Medium</value>
<value name="Large" display-name-key="Card_Size_Large">Large</value>
</property>
<property name="Alignment" display-name-key="Card_Alignment_Display_Key" description-key="Card_Alignment_Desc_Key" of-type="Enum" usage="input" default-value="Vertical">
<value name="Vertical" display-name-key="Card_Alignment_Vertical">Vertical</value>
<value name="Horizontal" display-name-key="Card_Alignment_Horizontal">Horizontal</value>
</property>
<!--AutoHeight property-->
<property name="AutoHeight" display-name-key="Card_AutoHeight_Display_Key" description-key="Card_AutoHeight_Desc_Key" usage="output" of-type="Whole.None" />
<!--Toolbar properties start-->
<data-set name="Items" display-name-key="Card_Toolbar_Items_Display_Key" description-key="Card_Toolbar_Items_Desc_Key" pfx-default-value="Table(
{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;})">
<property-set name="ItemDisplayName" display-name-key="Card_Toolbar_Items_ItemDisplayName" of-type="SingleLine.Text" usage="bound" required="true"/>
<property-set name="ItemKey" display-name-key="Card_Toolbar_Items_ItemKey" of-type="SingleLine.Text" usage="bound" required="true"/>
<property-set name="ItemDisabled" display-name-key="Card_Toolbar_Items_ItemDisabled" of-type="TwoOptions" usage="bound" required="false"/>
<property-set name="ItemVisible" display-name-key="Card_Toolbar_Items_ItemVisible" of-type="TwoOptions" usage="bound" required="false"/>
<property-set name="ItemIconName" display-name-key="Card_Toolbar_Items_ItemIconName" of-type="SingleLine.Text" usage="bound" required="false"/>
<property-set name="ItemIconStyle" display-name-key="Card_Toolbar_Items_ItemIconStyle" of-type="SingleLine.Text" usage="bound" required="false"/>
<property-set name="ItemAppearance" display-name-key="Card_Toolbar_Items_ItemAppearance" of-type="SingleLine.Text" usage="bound" required="false"/>
<property-set name="ItemTooltip" display-name-key="Card_Toolbar_Items_ItemTooltip" of-type="SingleLine.Text" usage="bound" required="false"/>
</data-set>
<!--Toolbar properties ends-->
<property name="TabIndex" hidden="true" display-name-key="NA" description-key="NA" of-type="Whole.None" usage="input" required="false"/>
<property name="Tooltip" hidden="true" display-name-key="NA" description-key="NA" of-type="SingleLine.Text" usage="input" required="false"/>
<common-property name="Height" default-value="110" pfx-default-value="Self.AutoHeight"/>
<common-property name="Width" default-value="300"/>
<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;))*/"/>
<resources>
<code path="index.ts" order="1"/>
<resx path="strings/Card.1033.resx" version="1.0.0"/>
<platform-library name="React" version="16.8.6"/>
<platform-library name="Fluent" version="9.19.1"/>
</resources>
<feature-usage>
<uses-feature name="ExplicitCommonEvents" required="true"/>
</feature-usage>
</control>
</manifest>
44 changes: 44 additions & 0 deletions Card/Card/ManifestConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const enum ItemColumns {
DisplayName = "ItemDisplayName",
Key = "ItemKey",
IconName = "ItemIconName",
IconStyle = "ItemIconStyle",
Visible = "ItemVisible",
Disabled = "ItemDisabled",
Appearance = "ItemAppearance",
Tooltip = "ItemTooltip",
}

export const enum StringConstants {
Regular = "Regular",
Subtle = "subtle",
AboveHeader = "Above header",
BelowHeader = "Below header",
}

export const Size = {
Large: "large",
Medium: "medium",
Small: "small",
};

export const Layout = {
"Icon only": "icon",
"Text only": "text",
"Icon before": "before",
"Icon after": "after",
"Icon above": "above",
};

export const enum ManifestPropertyNames {
dataset = "dataset",
}

export const Orientation = {
Vertical: "vertical",
Horizontal: "horizontal",
};

export const enum Alignment {
Alignment = "Alignment",
}
Loading

0 comments on commit 2d12d13

Please sign in to comment.