{
+ constructor(props: ITargetAudienceProps) {
+ super(props);
+ this.state = {
+ canView: false
+ } as ITargetAudienceState;
+
+ }
+ public componentDidMount(): void {
+ //setting the state whether user has permission to view webpart
+ this.checkUserCanViewWebpart();
+ }
+ public render(): JSX.Element {
+ return ({this.props.groupIds? (this.state.canView ?
+ this.props.children : ``):this.props.children}
);
+ }
+ public checkUserCanViewWebpart(): void {
+ const self = this;
+ let proms = [];
+ const errors = [];
+ const _sv = new spservices();
+ self.props.groupIds.map((item) => {
+ proms.push(_sv.isMember(item.fullName, self.props.pageContext.legacyPageContext[`userId`], self.props.pageContext.site.absoluteUrl));
+ });
+ Promise.race(
+ proms.map(p => {
+ return p.catch(err => {
+ errors.push(err);
+ if (errors.length >= proms.length) throw errors;
+ return Promise.race(null);
+ });
+ })).then(val => {
+ this.setState({ canView: true }); //atleast one promise resolved
+ });
+ }
+}
diff --git a/samples/react-target-audience/src/index.ts b/samples/react-target-audience/src/index.ts
new file mode 100644
index 000000000..289f09830
--- /dev/null
+++ b/samples/react-target-audience/src/index.ts
@@ -0,0 +1 @@
+// A file is required to be in the root of the /src directory by the TypeScript compiler
diff --git a/samples/react-target-audience/src/service/spservices.ts b/samples/react-target-audience/src/service/spservices.ts
new file mode 100644
index 000000000..823c47625
--- /dev/null
+++ b/samples/react-target-audience/src/service/spservices.ts
@@ -0,0 +1,30 @@
+import * as $ from 'jquery';
+export default class spservices{
+
+ constructor(){
+ }
+ /*check if user is a member of the group, using SP rest
+ */
+ public async isMember(groupName: string, userId: string, webAbsoluteUrl): Promise {
+ var p = new Promise((resolve, reject) => {
+ $.ajax({
+ url: webAbsoluteUrl + "/_api/web/sitegroups/getByName('" + groupName + "')/Users?$filter=Id eq " + userId,
+ method: "GET",
+ headers: { "Accept": "application/json; odata=verbose" },
+ success: (data) => {
+ if (data.d.results[0] != undefined) {
+ resolve(true);
+ }
+ else {
+ reject(false);
+ }
+ },
+ error: (error) => {
+ reject(false);
+ },
+ });
+ });
+ return p;
+}
+
+}
\ No newline at end of file
diff --git a/samples/react-target-audience/src/webparts/sampleTargetedComponent/SampleTargetedComponentWebPart.manifest.json b/samples/react-target-audience/src/webparts/sampleTargetedComponent/SampleTargetedComponentWebPart.manifest.json
new file mode 100644
index 000000000..428eda9e7
--- /dev/null
+++ b/samples/react-target-audience/src/webparts/sampleTargetedComponent/SampleTargetedComponentWebPart.manifest.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
+ "id": "eb85da40-ed83-4f29-b820-52a74b374483",
+ "alias": "SampleTargetedComponentWebPart",
+ "componentType": "WebPart",
+
+ // The "*" signifies that the version should be taken from the package.json
+ "version": "*",
+ "manifestVersion": 2,
+
+ // If true, the component can only be installed on sites where Custom Script is allowed.
+ // Components that allow authors to embed arbitrary script code should set this to true.
+ // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
+ "requiresCustomScript": false,
+ "supportedHosts": ["SharePointWebPart"],
+
+ "preconfiguredEntries": [{
+ "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
+ "group": { "default": "Other" },
+ "title": { "default": "Sample Targeted Component" },
+ "description": { "default": "Sample Targeted Component description" },
+ "officeFabricIconFontName": "Page",
+ "properties": {
+ "description": "Sample Targeted Component",
+ "groups" : []
+ }
+ }]
+}
diff --git a/samples/react-target-audience/src/webparts/sampleTargetedComponent/SampleTargetedComponentWebPart.ts b/samples/react-target-audience/src/webparts/sampleTargetedComponent/SampleTargetedComponentWebPart.ts
new file mode 100644
index 000000000..c3285d002
--- /dev/null
+++ b/samples/react-target-audience/src/webparts/sampleTargetedComponent/SampleTargetedComponentWebPart.ts
@@ -0,0 +1,92 @@
+import * as React from 'react';
+import * as ReactDom from 'react-dom';
+import { DisplayMode, Version } from '@microsoft/sp-core-library';
+import {
+ IPropertyPaneConfiguration,
+ PropertyPaneTextField
+} from '@microsoft/sp-property-pane';
+import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
+
+import * as strings from 'SampleTargetedComponentWebPartStrings';
+import SampleTargetedComponent from './components/SampleTargetedComponent';
+import { ISampleTargetedComponentProps } from './components/ISampleTargetedComponentProps';
+import { PropertyFieldPeoplePicker, IPropertyFieldGroupOrPerson, PrincipalType } from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
+import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
+export interface ISampleTargetedComponentWebPartProps {
+ description: string;
+ groups: IPropertyFieldGroupOrPerson[];
+}
+
+export default class SampleTargetedComponentWebPart extends BaseClientSideWebPart {
+
+ public render(): void {
+ if (
+ this.displayMode === DisplayMode.Edit &&
+ this.properties.groups.length === 0
+ ) {
+ const placeHolderElement = React.createElement(Placeholder, {
+ iconName: "Edit",
+ iconText: "Configure your web part",
+ description: "Please configure the web part.",
+ buttonLabel: "Configure",
+ onConfigure: this._onConfigure,
+ });
+ ReactDom.render(placeHolderElement, this.domElement);
+ } else {
+ const element: React.ReactElement = React.createElement(
+ SampleTargetedComponent,
+ {
+ pageContext: this.context.pageContext,
+ groupIds: this.properties.groups,
+ description: this.properties.description,
+ }
+ );
+ ReactDom.render(element, this.domElement);
+ }
+ }
+ protected _onConfigure = () => {
+ // Context of the web part
+ this.context.propertyPane.open();
+ }
+ protected onDispose(): void {
+ ReactDom.unmountComponentAtNode(this.domElement);
+ }
+
+ protected get dataVersion(): Version {
+ return Version.parse('1.0');
+ }
+
+ protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
+ return {
+ pages: [
+ {
+ header: {
+ description: strings.PropertyPaneDescription
+ },
+ groups: [
+ {
+ groupName: strings.BasicGroupName,
+ groupFields: [
+ PropertyPaneTextField('description', {
+ label: strings.DescriptionFieldLabel
+ }),
+ PropertyFieldPeoplePicker('groups', {
+ label: 'Target Audience',
+ initialData: this.properties.groups,
+ allowDuplicate: false,
+ principalType: [PrincipalType.SharePoint],
+ onPropertyChange: this.onPropertyPaneFieldChanged,
+ context: this.context,
+ properties: this.properties,
+ onGetErrorMessage: null,
+ deferredValidationTime: 0,
+ key: 'peopleFieldId'
+ })
+ ]
+ }
+ ]
+ }
+ ]
+ };
+ }
+}
\ No newline at end of file
diff --git a/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/ISampleTargetedComponentProps.ts b/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/ISampleTargetedComponentProps.ts
new file mode 100644
index 000000000..fb7ae0001
--- /dev/null
+++ b/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/ISampleTargetedComponentProps.ts
@@ -0,0 +1,5 @@
+
+import { ITargetAudienceProps } from '../../../common/TargetAudience';
+export interface ISampleTargetedComponentProps extends ITargetAudienceProps {
+ description: string;
+}
diff --git a/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/SampleTargetedComponent.module.scss b/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/SampleTargetedComponent.module.scss
new file mode 100644
index 000000000..13f5a5ab1
--- /dev/null
+++ b/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/SampleTargetedComponent.module.scss
@@ -0,0 +1,74 @@
+@import '~office-ui-fabric-react/dist/sass/References.scss';
+
+.sampleTargetedComponent {
+ .container {
+ max-width: 700px;
+ margin: 0px auto;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+ }
+
+ .row {
+ @include ms-Grid-row;
+ @include ms-fontColor-white;
+ background-color: $ms-color-themeDark;
+ padding: 20px;
+ }
+
+ .column {
+ @include ms-Grid-col;
+ @include ms-lg10;
+ @include ms-xl8;
+ @include ms-xlPush2;
+ @include ms-lgPush1;
+ }
+
+ .title {
+ @include ms-font-xl;
+ @include ms-fontColor-white;
+ }
+
+ .subTitle {
+ @include ms-font-l;
+ @include ms-fontColor-white;
+ }
+
+ .description {
+ @include ms-font-l;
+ @include ms-fontColor-white;
+ }
+
+ .button {
+ // Our button
+ text-decoration: none;
+ height: 32px;
+
+ // Primary Button
+ min-width: 80px;
+ background-color: $ms-color-themePrimary;
+ border-color: $ms-color-themePrimary;
+ color: $ms-color-white;
+
+ // Basic Button
+ outline: transparent;
+ position: relative;
+ font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
+ -webkit-font-smoothing: antialiased;
+ font-size: $ms-font-size-m;
+ font-weight: $ms-font-weight-regular;
+ border-width: 0;
+ text-align: center;
+ cursor: pointer;
+ display: inline-block;
+ padding: 0 16px;
+
+ .label {
+ font-weight: $ms-font-weight-semibold;
+ font-size: $ms-font-size-m;
+ height: 32px;
+ line-height: 32px;
+ margin: 0 4px;
+ vertical-align: top;
+ display: inline-block;
+ }
+ }
+}
diff --git a/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/SampleTargetedComponent.tsx b/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/SampleTargetedComponent.tsx
new file mode 100644
index 000000000..9746b528e
--- /dev/null
+++ b/samples/react-target-audience/src/webparts/sampleTargetedComponent/components/SampleTargetedComponent.tsx
@@ -0,0 +1,37 @@
+import * as React from "react";
+import styles from "./SampleTargetedComponent.module.scss";
+import { ISampleTargetedComponentProps } from "./ISampleTargetedComponentProps";
+import TargetAudience, {
+ ITargetAudienceState
+} from "../../../common/TargetAudience";
+export interface ISampleTargetedComponentState extends ITargetAudienceState {
+ description?: string;
+}
+export default class SampleTargetedComponent extends React.Component {
+ constructor(props: ISampleTargetedComponentProps) {
+ super(props);
+ this.state = {
+ description: this.props.description
+ };
+ }
+
+ public render(): JSX.Element {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/samples/react-target-audience/src/webparts/sampleTargetedComponent/loc/en-us.js b/samples/react-target-audience/src/webparts/sampleTargetedComponent/loc/en-us.js
new file mode 100644
index 000000000..e5f5a6025
--- /dev/null
+++ b/samples/react-target-audience/src/webparts/sampleTargetedComponent/loc/en-us.js
@@ -0,0 +1,7 @@
+define([], function() {
+ return {
+ "PropertyPaneDescription": "Description",
+ "BasicGroupName": "Group Name",
+ "DescriptionFieldLabel": "Description Field"
+ }
+});
\ No newline at end of file
diff --git a/samples/react-target-audience/src/webparts/sampleTargetedComponent/loc/mystrings.d.ts b/samples/react-target-audience/src/webparts/sampleTargetedComponent/loc/mystrings.d.ts
new file mode 100644
index 000000000..d212be6ac
--- /dev/null
+++ b/samples/react-target-audience/src/webparts/sampleTargetedComponent/loc/mystrings.d.ts
@@ -0,0 +1,10 @@
+declare interface ISampleTargetedComponentWebPartStrings {
+ PropertyPaneDescription: string;
+ BasicGroupName: string;
+ DescriptionFieldLabel: string;
+}
+
+declare module 'SampleTargetedComponentWebPartStrings' {
+ const strings: ISampleTargetedComponentWebPartStrings;
+ export = strings;
+}
diff --git a/samples/react-target-audience/teams/eb85da40-ed83-4f29-b820-52a74b374483_color.png b/samples/react-target-audience/teams/eb85da40-ed83-4f29-b820-52a74b374483_color.png
new file mode 100644
index 000000000..a8d279707
Binary files /dev/null and b/samples/react-target-audience/teams/eb85da40-ed83-4f29-b820-52a74b374483_color.png differ
diff --git a/samples/react-target-audience/teams/eb85da40-ed83-4f29-b820-52a74b374483_outline.png b/samples/react-target-audience/teams/eb85da40-ed83-4f29-b820-52a74b374483_outline.png
new file mode 100644
index 000000000..6df4a038d
Binary files /dev/null and b/samples/react-target-audience/teams/eb85da40-ed83-4f29-b820-52a74b374483_outline.png differ
diff --git a/samples/react-target-audience/tsconfig.json b/samples/react-target-audience/tsconfig.json
new file mode 100644
index 000000000..72ce74214
--- /dev/null
+++ b/samples/react-target-audience/tsconfig.json
@@ -0,0 +1,38 @@
+{
+ "extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
+ "compilerOptions": {
+ "target": "es5",
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "jsx": "react",
+ "declaration": true,
+ "sourceMap": true,
+ "experimentalDecorators": true,
+ "skipLibCheck": true,
+ "outDir": "lib",
+ "inlineSources": false,
+ "strictNullChecks": false,
+ "noUnusedLocals": false,
+ "typeRoots": [
+ "./node_modules/@types",
+ "./node_modules/@microsoft"
+ ],
+ "types": [
+ "es6-promise",
+ "webpack-env"
+ ],
+ "lib": [
+ "es5",
+ "dom",
+ "es2015.collection"
+ ]
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "lib"
+ ]
+}
diff --git a/samples/react-target-audience/tslint.json b/samples/react-target-audience/tslint.json
new file mode 100644
index 000000000..6bfc75a4c
--- /dev/null
+++ b/samples/react-target-audience/tslint.json
@@ -0,0 +1,30 @@
+{
+ "extends": "@microsoft/sp-tslint-rules/base-tslint.json",
+ "rules": {
+ "class-name": false,
+ "export-name": false,
+ "forin": false,
+ "label-position": false,
+ "member-access": true,
+ "no-arg": false,
+ "no-console": false,
+ "no-construct": false,
+ "no-duplicate-variable": true,
+ "no-eval": false,
+ "no-function-expression": true,
+ "no-internal-module": true,
+ "no-shadowed-variable": true,
+ "no-switch-case-fall-through": true,
+ "no-unnecessary-semicolons": true,
+ "no-unused-expression": true,
+ "no-use-before-declare": true,
+ "no-with-statement": true,
+ "semicolon": true,
+ "trailing-comma": false,
+ "typedef": false,
+ "typedef-whitespace": false,
+ "use-named-parameter": true,
+ "variable-name": false,
+ "whitespace": false
+ }
+}
\ No newline at end of file