From 9e95d7c13f4f05a6028ba7f45f186e5a4c80dbc1 Mon Sep 17 00:00:00 2001 From: Asish Padhy Date: Wed, 7 Mar 2018 22:21:57 +1100 Subject: [PATCH 01/15] Initiated Peoplepicker control --- src/PeoplePicker.ts | 1 + src/controls/peoplepicker/IPeoplePicker.ts | 76 +++++ .../PeoplePickerComponent.module.scss | 100 ++++++ .../peoplepicker/SPPeoplePickerComponent.tsx | 286 ++++++++++++++++++ src/controls/peoplepicker/index.ts | 2 + .../controlsTest/components/ControlsTest.tsx | 61 +++- 6 files changed, 520 insertions(+), 6 deletions(-) create mode 100644 src/PeoplePicker.ts create mode 100644 src/controls/peoplepicker/IPeoplePicker.ts create mode 100644 src/controls/peoplepicker/PeoplePickerComponent.module.scss create mode 100644 src/controls/peoplepicker/SPPeoplePickerComponent.tsx create mode 100644 src/controls/peoplepicker/index.ts diff --git a/src/PeoplePicker.ts b/src/PeoplePicker.ts new file mode 100644 index 000000000..0cb34e74f --- /dev/null +++ b/src/PeoplePicker.ts @@ -0,0 +1 @@ +export * from './controls/peoplepicker/index'; diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts new file mode 100644 index 000000000..84b98243e --- /dev/null +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -0,0 +1,76 @@ +import { IPersonaProps, DirectionalHint } from "office-ui-fabric-react"; +import { WebPartContext } from '@microsoft/sp-webpart-base'; + +/** + * Used to display a placeholder in case of no or temporary content. Button is optional. + * + */ +export interface IPeoplePickerProps { + /** + * Context of the component + */ + context: WebPartContext; + /** + * Name of SharePoint Group + */ + groupName?: string; + /** + * image Initials + */ + getAllUsers?: boolean; + /** + * Text of the Control + */ + titleText: string; + /** + * Selection Limit of Control + */ + personSelectionLimit?: number; + /** + * Show or Hide Tooltip + */ + showtooltip? : boolean; + /** + * People Field is mandatory + */ + isRequired? : boolean; + /** + * Mandatory field error message + */ + errorMessage? : string; + /** + * Method to check value of People Picker text + */ + selectedItems?: (items: any[]) => void; + /** + * Tooltip Message + */ + tooltipMessage? : string; + /** + * Directional Hint of tool tip + */ + tooltipDirectional? : DirectionalHint; +} + +export interface IPeoplePickerState { + selectedPersons?: IPersonaProps[]; + mostRecentlyUsedPersons: IPersonaProps[]; + currentSelectedPersons: IPersonaProps[]; + allPersons: [{ + id: string, + imageUrl: string, + imageInitials: string, + primaryText: string, //Name + secondaryText: string, //Role + tertiaryText: string, //status + optionalText: string //stgring + }]; + delayResults?: boolean; + currentPicker?: number | string; + peoplePersonaMenu?: IPersonaProps[]; + peoplePartTitle: string; + peoplePartTooltip : string; + isLoading : boolean; + peopleValidatorText? : string; + showmessageerror: boolean; +} diff --git a/src/controls/peoplepicker/PeoplePickerComponent.module.scss b/src/controls/peoplepicker/PeoplePickerComponent.module.scss new file mode 100644 index 000000000..451e3d3a8 --- /dev/null +++ b/src/controls/peoplepicker/PeoplePickerComponent.module.scss @@ -0,0 +1,100 @@ +.placeholder { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + + .placeholderContainer { + -webkit-box-align: center; + -ms-flex-align: center; + -ms-grid-row-align: center; + align-items: center; + color: "[theme:neutralSecondary, default: #666666]"; + background-color: "[theme:neutralLighter, default: #f4f4f4]"; + width: 100%; + padding: 80px 0; + + .placeholderHead { + color: "[theme:neutralPrimary, default: #333333]"; + + .placeholderHeadContainer { + height: 100%; + white-space: nowrap; + text-align: center; + } + + .placeholderIcon { + display: inline-block; + vertical-align: middle; + white-space: normal; + } + + .placeholderText { + display: inline-block; + vertical-align: middle; + white-space: normal + } + } + + .placeholderDescription { + width: 65%; + vertical-align: middle; + margin: 0 auto; + text-align: center; + + .placeholderDescriptionText { + color: "[theme:neutralSecondary, default: #666666]"; + font-size: 17px; + display: inline-block; + margin: 24px 0; + font-weight: 100; + } + + button { + font-size: 14px; + font-weight: 400; + box-sizing: border-box; + display: inline-block; + text-align: center; + cursor: pointer; + vertical-align: top; + min-width: 80px; + height: 32px; + background-color: "[theme:themePrimary, default: #0078d7]"; + color: #fff; + user-select: none; + outline: transparent; + border-width: 1px; + border-style: solid; + border-color: transparent; + border-image: initial; + text-decoration: none; + } + } + } +} + +[dir=ltr] .placeholder, +[dir=rtl] .placeholder { + + .placeholderContainer { + + .placeholderHead { + + .placeholderText { + padding-left: 20px; + } + } + } +} + +.placeholderOverlay { + position: relative; + height: 100%; + z-index: 1; + + .placeholderSpinnerContainer { + position: relative; + width: 100%; + margin: 164px 0 + } +} diff --git a/src/controls/peoplepicker/SPPeoplePickerComponent.tsx b/src/controls/peoplepicker/SPPeoplePickerComponent.tsx new file mode 100644 index 000000000..27f5032e7 --- /dev/null +++ b/src/controls/peoplepicker/SPPeoplePickerComponent.tsx @@ -0,0 +1,286 @@ +import * as React from 'react'; +import { IPeoplePickerProps, IPeoplePickerState } from './IPeoplePicker'; +import { Persona, IPersonaProps } from 'office-ui-fabric-react/lib/Persona'; +import { TooltipHost, DirectionalHint } from 'office-ui-fabric-react/lib/Tooltip'; +import { IBasePickerSuggestionsProps } from 'office-ui-fabric-react/lib/Pickers'; +import { NormalPeoplePicker } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePicker'; +import { IPersonaWithMenu } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePickerItems/PeoplePickerItem.Props'; +import { ValidationState } from 'office-ui-fabric-react/lib/Pickers'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; +import { SPHttpClient } from '@microsoft/sp-http'; +import styles from './PeoplePickerComponent.module.scss'; +import * as appInsights from '../../common/appInsights'; +import { + assign +} from 'office-ui-fabric-react/lib/Utilities'; +import { autobind } from 'office-ui-fabric-react'; + +const suggestionProps: IBasePickerSuggestionsProps = { + suggestionsHeaderText: 'Suggested People', + noResultsFoundText: 'No results found', + loadingText: 'Loading' +}; + +/** +* PeoplePicker component +*/ +export class SPPeoplePicker extends React.Component { + + public static defaultProps: IPeoplePickerProps = { + context : null, + getAllUsers: true, + titleText: "People Picker", + personSelectionLimit: 1, + showtooltip : false, + isRequired : false, + errorMessage : "People picker is mandatory", + groupName: "", + tooltipMessage: "This is a People Picker", + tooltipDirectional: DirectionalHint.leftTopEdge + }; + +/** +* Constructor +*/ +constructor(props: IPeoplePickerProps) { + super(props); + this.state = { + selectedPersons: [], + mostRecentlyUsedPersons: [], + currentSelectedPersons: [], + allPersons: [{ + id: "", + imageUrl: "", + imageInitials: "", + primaryText: "", //Name + secondaryText: "", //Role + tertiaryText: "", //status + optionalText: "" //anything + }], + delayResults: false, + currentPicker: 0, + peoplePartTitle: "", + peoplePartTooltip : "", + isLoading : false, + showmessageerror: false + }; + + if (typeof this.props.selectedItems !== 'undefined' && this.props.selectedItems !== null) { + this.props.selectedItems(this.state.selectedPersons); + }; + + appInsights.track('ReactPeoplePicker', { + groupName: !!props.groupName, + name: !!props.groupName, + getAllUsers: !!props.getAllUsers, + titleText: !!props.titleText + }); + + this._onPersonItemsChange = this._onPersonItemsChange.bind(this); + } + + public componentWillMount(): void { + this._thisLoadUsers(); + } + + private generateUserPhotoLink(value : string) : string { + return `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${value}&UA=0&size=HR96x96` + } + + private _thisLoadUsers() : void { + var stringVal = ""; + if(this.props.getAllUsers) + { + stringVal = "/_api/web/siteusers"; + } + else if(this.props.groupName != "") + { + stringVal = `/_api/web/sitegroups/GetByName('${this.props.groupName}')/users`; + } + + const restApi = `${this.props.context.pageContext.web.absoluteUrl}${stringVal}`; + this.props.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1) + .then(resp => { return resp.json(); }) + .then(items => { + var userValuesArray : any = [{ + id: 0, + imageUrl: "", + imageInitials: "", + primaryText: "", //Name + secondaryText: "", //Role + tertiaryText: "", //status + optionalText: "" //anything + }]; + + for(let i = 0; i < items.value.length; i++) + { + if(i == 0) + { + userValuesArray = [{ + id: items.value[i].Id, + //imageUrl: `/_layouts/15/userphoto.aspx?size=S&accountname=${items.value[i].Email}`, + imageUrl: this.generateUserPhotoLink(items.value[i].Email), // `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${items.value[i].Email}&UA=0&size=HR96x96`, + imageInitials: "", + primaryText: items.value[i].Title, //Name + secondaryText: items.value[i].Email, //Role + tertiaryText: "", //status + optionalText: "" //anything + }] + } + else + { + userValuesArray.push({ + id: items.value[i].Id, + imageUrl: this.generateUserPhotoLink(items.value[i].Email), //`https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${items.value[i].Email}&UA=0&size=HR96x96`, + imageInitials: "", + primaryText: items.value[i].Title, //Name + secondaryText: items.value[i].Email, //Role + tertiaryText: "", //status + optionalText: "" //anything + }); + } + } + + let personaList: IPersonaWithMenu[] = []; + userValuesArray.forEach((persona: IPersonaProps) => { + let personaWithMenu: IPersonaWithMenu = {}; + assign(personaWithMenu, persona) + personaList.push(personaWithMenu); + }); + + this.setState({ + allPersons : userValuesArray, + peoplePersonaMenu : personaList, + mostRecentlyUsedPersons : personaList.slice(0,5), + showmessageerror: this.props.isRequired && this.state.selectedPersons.length === 0 + }); + }); + } + +@autobind +private _onPersonItemsChange(items: any[]) { + this.setState({ + selectedPersons: items, + showmessageerror: items.length > 0 ? false : true + }); + } + +@autobind +private _validateInputPeople(input: string) { + if (input.indexOf('@') !== -1) { + return ValidationState.valid; + } else if (input.length > 1) { + return ValidationState.warning; + } else { + return ValidationState.invalid; + } +} + +@autobind +private _returnMostRecentlyUsedPerson(currentPersonas: IPersonaProps[]): IPersonaProps[] | Promise { + let { mostRecentlyUsedPersons } = this.state; + mostRecentlyUsedPersons = this._removeDuplicates(mostRecentlyUsedPersons, currentPersonas); + return this._filterPromise(mostRecentlyUsedPersons); +} + +@autobind +private _onPersonFilterChanged(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) { + if (filterText) { + let filteredPersonas: IPersonaProps[] = this._filterPersons(filterText); + + filteredPersonas = this._removeDuplicates(filteredPersonas, currentPersonas); + filteredPersonas = limitResults ? filteredPersonas.splice(0, limitResults) : filteredPersonas; + return this._filterPromise(filteredPersonas); + } else { + return []; + } +} + +@autobind +private _filterPersons(filterText: string): IPersonaProps[] { + return this.state.peoplePersonaMenu.filter(item => this._doesTextStartWith(item.primaryText as string, filterText)); +} + +@autobind +private _removeDuplicates(personas: IPersonaProps[], possibleDupes: IPersonaProps[]) { + return personas.filter(persona => !this._listContainsPersona(persona, possibleDupes)); +} + +@autobind +private _doesTextStartWith(text: string, filterText: string): boolean { + return text.toLowerCase().indexOf(filterText.toLowerCase()) === 0; +} + +@autobind +private _listContainsPersona(persona: IPersonaProps, personas: IPersonaProps[]) { + if (!personas || !personas.length || personas.length === 0) { + return false; + } + return personas.filter(item => item.primaryText === persona.primaryText).length > 0; +} + +@autobind +private _filterPromise(personasToReturn: IPersonaProps[]): IPersonaProps[] | Promise { + if (this.state.delayResults) { + return this._convertResultsToPromise(personasToReturn); + } else { + return personasToReturn; + } +} + +@autobind +private _convertResultsToPromise(results: IPersonaProps[]): Promise { + return new Promise((resolve, reject) => setTimeout(() => resolve(results), 2000)); +} + + //#endregion User control function and bindings + +/** + * Default React component render method + */ +public render(): React.ReactElement { + const peoplepicker =
{this.props.titleText} + peoplePersonaMenu.primaryText} + className={ 'ms-PeoplePicker' } + key={ 'normal' } + onValidateInput={ this._validateInputPeople } + removeButtonAriaLabel={ 'Remove' } + inputProps={ { + 'aria-label': 'People Picker', + onBlur: (ev: React.FocusEvent) => console.log('onBlur on People Picker called'), + onFocus: (ev: React.FocusEvent) => console.log('onFocus on People Picker called'), + } } + itemLimit={this.props.personSelectionLimit} + onChange = { this._onPersonItemsChange } + /> +
; + return ( +
+ {this.props.showtooltip ? + + {peoplepicker} + : +
+ {peoplepicker} +
+ } + {(this.props.isRequired && this.state.showmessageerror) ? + + {this.props.errorMessage} + : null + } +
+ ); + } +} + + + diff --git a/src/controls/peoplepicker/index.ts b/src/controls/peoplepicker/index.ts new file mode 100644 index 000000000..f19c190c6 --- /dev/null +++ b/src/controls/peoplepicker/index.ts @@ -0,0 +1,2 @@ +export * from './IPeoplePicker'; +export * from './SPPeoplePickerComponent'; \ No newline at end of file diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index cf1ad2c1d..a778e1854 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -39,7 +39,8 @@ export default class ControlsTest extends React.Component { return resp.json(); }) .then(items => { @@ -102,6 +103,12 @@ export default class ControlsTest extends React.Component { + // return Link; + // } + // }, + // { + // name: 'Title' + // } + // ]; + // Specify the fields that need to be viewed in the listview const viewFields: IViewField[] = [ { - name: 'ListItemAllFields.Id', + name: 'Id', displayName: 'ID', maxWidth: 40, sorting: true }, { - name: 'ListItemAllFields.Underscore_Field', - displayName: "Underscore_Field", + name: 'Students_x0020_Strength', + displayName: "Students Strength", sorting: true }, { - name: 'Name', - linkPropertyName: 'ServerRelativeUrl', + name: 'Training_x0020_site.Description', + linkPropertyName: 'Training_x0020_site.Url', + displayName: 'Training Site', sorting: true }, { @@ -273,6 +311,17 @@ export default class ControlsTest extends React.Component

Deletes second item

+ + ); } From c33336c6bd75049a94ef69c235bf10d42423b8dc Mon Sep 17 00:00:00 2001 From: Asish Padhy Date: Wed, 7 Mar 2018 22:21:57 +1100 Subject: [PATCH 02/15] People Picker control ready for testing --- ...omponent.tsx => PeoplePickerComponent.tsx} | 13 +++-- src/controls/peoplepicker/index.ts | 2 +- .../controlsTest/components/ControlsTest.tsx | 49 ++++--------------- 3 files changed, 16 insertions(+), 48 deletions(-) rename src/controls/peoplepicker/{SPPeoplePickerComponent.tsx => PeoplePickerComponent.tsx} (93%) diff --git a/src/controls/peoplepicker/SPPeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx similarity index 93% rename from src/controls/peoplepicker/SPPeoplePickerComponent.tsx rename to src/controls/peoplepicker/PeoplePickerComponent.tsx index 27f5032e7..475d4ad75 100644 --- a/src/controls/peoplepicker/SPPeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -25,7 +25,7 @@ const suggestionProps: IBasePickerSuggestionsProps = { /** * PeoplePicker component */ -export class SPPeoplePicker extends React.Component { +export class PeoplePicker extends React.Component { public static defaultProps: IPeoplePickerProps = { context : null, @@ -108,7 +108,7 @@ constructor(props: IPeoplePickerProps) { imageUrl: "", imageInitials: "", primaryText: "", //Name - secondaryText: "", //Role + secondaryText: "", //Email tertiaryText: "", //status optionalText: "" //anything }]; @@ -119,11 +119,10 @@ constructor(props: IPeoplePickerProps) { { userValuesArray = [{ id: items.value[i].Id, - //imageUrl: `/_layouts/15/userphoto.aspx?size=S&accountname=${items.value[i].Email}`, - imageUrl: this.generateUserPhotoLink(items.value[i].Email), // `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${items.value[i].Email}&UA=0&size=HR96x96`, + imageUrl: this.generateUserPhotoLink(items.value[i].Email), imageInitials: "", primaryText: items.value[i].Title, //Name - secondaryText: items.value[i].Email, //Role + secondaryText: items.value[i].Email, //Email tertiaryText: "", //status optionalText: "" //anything }] @@ -132,10 +131,10 @@ constructor(props: IPeoplePickerProps) { { userValuesArray.push({ id: items.value[i].Id, - imageUrl: this.generateUserPhotoLink(items.value[i].Email), //`https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${items.value[i].Email}&UA=0&size=HR96x96`, + imageUrl: this.generateUserPhotoLink(items.value[i].Email), imageInitials: "", primaryText: items.value[i].Title, //Name - secondaryText: items.value[i].Email, //Role + secondaryText: items.value[i].Email, //Email tertiaryText: "", //status optionalText: "" //anything }); diff --git a/src/controls/peoplepicker/index.ts b/src/controls/peoplepicker/index.ts index f19c190c6..675051271 100644 --- a/src/controls/peoplepicker/index.ts +++ b/src/controls/peoplepicker/index.ts @@ -1,2 +1,2 @@ export * from './IPeoplePicker'; -export * from './SPPeoplePickerComponent'; \ No newline at end of file +export * from './PeoplePickerComponent'; \ No newline at end of file diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index a778e1854..ff25c4a73 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -17,6 +17,7 @@ import { IFrameDialog } from '../../../IFrameDialog'; import { Environment, EnvironmentType } from '@microsoft/sp-core-library'; import { SecurityTrimmedControl, PermissionLevel } from '../../../SecurityTrimmedControl'; import { SPPermission } from '@microsoft/sp-page-context'; +import { PeoplePicker } from '../../../PeoplePicker'; /** * Component that can be used to test out the React controls from this project @@ -39,8 +40,7 @@ export default class ControlsTest extends React.Component { return resp.json(); }) .then(items => { @@ -133,53 +133,22 @@ export default class ControlsTest extends React.Component { - // return Link; - // } - // }, - // { - // name: 'Title' - // } - // ]; - // Specify the fields that need to be viewed in the listview const viewFields: IViewField[] = [ { - name: 'Id', + name: 'ListItemAllFields.Id', displayName: 'ID', maxWidth: 40, sorting: true }, { - name: 'Students_x0020_Strength', - displayName: "Students Strength", + name: 'ListItemAllFields.Underscore_Field', + displayName: "Underscore_Field", sorting: true }, { - name: 'Training_x0020_site.Description', - linkPropertyName: 'Training_x0020_site.Url', - displayName: 'Training Site', + name: 'Name', + linkPropertyName: 'ServerRelativeUrl', sorting: true }, { @@ -310,9 +279,9 @@ export default class ControlsTest extends React.Component -

Deletes second item

+

Deletes second item

- Date: Thu, 15 Mar 2018 22:50:08 +1100 Subject: [PATCH 03/15] Peoplepicker ready. Properties and validation updates pending --- src/controls/peoplepicker/PeoplePickerComponent.tsx | 1 + src/webparts/controlsTest/components/ControlsTest.tsx | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index 475d4ad75..783ec86f8 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -45,6 +45,7 @@ export class PeoplePicker extends React.Component Date: Mon, 30 Apr 2018 21:10:51 +1000 Subject: [PATCH 04/15] Peoplepicker control v1.0 ready with validation and properties --- src/controls/peoplepicker/IPeoplePicker.ts | 12 +++ .../PeoplePickerComponent.module.scss | 101 +----------------- .../peoplepicker/PeoplePickerComponent.tsx | 6 +- 3 files changed, 17 insertions(+), 102 deletions(-) diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts index 84b98243e..2d13982e5 100644 --- a/src/controls/peoplepicker/IPeoplePicker.ts +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -50,6 +50,18 @@ export interface IPeoplePickerProps { * Directional Hint of tool tip */ tooltipDirectional? : DirectionalHint; + /** + * Class Name for the whole People picker control + */ + peoplePickerWPclassName?:string; + /** + * Class Name for the People picker control + */ + peoplePickerCntrlclassName?: string; + /** + * Class Name for the Error Section + */ + errorMessageclassName?: string; } export interface IPeoplePickerState { diff --git a/src/controls/peoplepicker/PeoplePickerComponent.module.scss b/src/controls/peoplepicker/PeoplePickerComponent.module.scss index 451e3d3a8..1b3e8cef4 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.module.scss +++ b/src/controls/peoplepicker/PeoplePickerComponent.module.scss @@ -1,100 +1,3 @@ -.placeholder { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - - .placeholderContainer { - -webkit-box-align: center; - -ms-flex-align: center; - -ms-grid-row-align: center; - align-items: center; - color: "[theme:neutralSecondary, default: #666666]"; - background-color: "[theme:neutralLighter, default: #f4f4f4]"; - width: 100%; - padding: 80px 0; - - .placeholderHead { - color: "[theme:neutralPrimary, default: #333333]"; - - .placeholderHeadContainer { - height: 100%; - white-space: nowrap; - text-align: center; - } - - .placeholderIcon { - display: inline-block; - vertical-align: middle; - white-space: normal; - } - - .placeholderText { - display: inline-block; - vertical-align: middle; - white-space: normal - } - } - - .placeholderDescription { - width: 65%; - vertical-align: middle; - margin: 0 auto; - text-align: center; - - .placeholderDescriptionText { - color: "[theme:neutralSecondary, default: #666666]"; - font-size: 17px; - display: inline-block; - margin: 24px 0; - font-weight: 100; - } - - button { - font-size: 14px; - font-weight: 400; - box-sizing: border-box; - display: inline-block; - text-align: center; - cursor: pointer; - vertical-align: top; - min-width: 80px; - height: 32px; - background-color: "[theme:themePrimary, default: #0078d7]"; - color: #fff; - user-select: none; - outline: transparent; - border-width: 1px; - border-style: solid; - border-color: transparent; - border-image: initial; - text-decoration: none; - } - } - } -} - -[dir=ltr] .placeholder, -[dir=rtl] .placeholder { - - .placeholderContainer { - - .placeholderHead { - - .placeholderText { - padding-left: 20px; - } - } - } -} - -.placeholderOverlay { - position: relative; - height: 100%; - z-index: 1; - - .placeholderSpinnerContainer { - position: relative; - width: 100%; - margin: 164px 0 - } +.defaultClass { + color : black; } diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index 783ec86f8..1ac78e75f 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -45,7 +45,6 @@ export class PeoplePicker extends React.Component { - const peoplepicker =
{this.props.titleText} + const peoplepicker =
{this.props.titleText} peoplePersonaMenu.primaryText} - className={ 'ms-PeoplePicker' } + className={ `'ms-PeoplePicker' ${this.props.peoplePickerCntrlclassName ? this.props.peoplePickerCntrlclassName : ''}` } key={ 'normal' } onValidateInput={ this._validateInputPeople } removeButtonAriaLabel={ 'Remove' } @@ -273,6 +272,7 @@ public render(): React.ReactElement { {this.props.errorMessage} : null From 62609d0e2c9911558f722672f53cff4bb76ff1e5 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 7 Jun 2018 13:24:35 +0200 Subject: [PATCH 05/15] Enhancement #82 --- CHANGELOG.md | 6 ++++ .../documentation/docs/about/release-notes.md | 6 ++++ .../docs/controls/TaxonomyPicker.md | 1 + package.json | 2 +- .../taxonomyPicker/ITaxonomyPicker.ts | 5 +++ .../taxonomyPicker/TaxonomyPicker.tsx | 11 +++--- src/controls/taxonomyPicker/Term.tsx | 17 ++++++++- src/controls/taxonomyPicker/TermParent.tsx | 35 ++++++++++--------- src/controls/taxonomyPicker/TermPicker.tsx | 9 ++++- .../controlsTest/components/ControlsTest.tsx | 3 +- 10 files changed, 68 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d69ec1c..e3ea59ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Releases +## 1.5.0 + +**Enhancements** + +- Added a property to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) + ## 1.4.0 **New Controls** diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index a8d69ec1c..e3ea59ea0 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -1,5 +1,11 @@ # Releases +## 1.5.0 + +**Enhancements** + +- Added a property to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) + ## 1.4.0 **New Controls** diff --git a/docs/documentation/docs/controls/TaxonomyPicker.md b/docs/documentation/docs/controls/TaxonomyPicker.md index 50092626e..3b5866c7e 100644 --- a/docs/documentation/docs/controls/TaxonomyPicker.md +++ b/docs/documentation/docs/controls/TaxonomyPicker.md @@ -68,6 +68,7 @@ The TaxonomyPicker control can be configured with the following properties: | termsetNameOrID | string | yes | The name or Id of your TermSet that you would like the Taxonomy Picker to chose terms from. | | onChange | function | no | captures the event of when the terms in the picker has changed. | | isTermSetSelectable | boolean | no | Specify if the TermSet itself is selectable in the tree view. | +| disabledTermIds | string[] | no | Specify which terms should be disabled in the term set so that they cannot be selected. | | anchorId | string | no | Set the anchorid to a child term in the TermSet to be able to select terms from that level and below. | Interface `IPickerTerm` diff --git a/package.json b/package.json index fdc9a81aa..b6b99824d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@pnp/spfx-controls-react", "description": "Reusable React controls for SharePoint Framework solutions", - "version": "1.4.0", + "version": "1.5.0", "engines": { "node": ">=0.10.0" }, diff --git a/src/controls/taxonomyPicker/ITaxonomyPicker.ts b/src/controls/taxonomyPicker/ITaxonomyPicker.ts index 8492c6157..1c93744fe 100644 --- a/src/controls/taxonomyPicker/ITaxonomyPicker.ts +++ b/src/controls/taxonomyPicker/ITaxonomyPicker.ts @@ -40,6 +40,10 @@ export interface ITaxonomyPickerProps { * Specify if the term set itself is selectable in the tree view */ isTermSetSelectable?: boolean; + /** + * Specify which terms should be disabled in the term set so that they cannot be selected + */ + disabledTermIds?: string[]; /** * Whether the property pane field is enabled or not. */ @@ -80,6 +84,7 @@ export interface ITaxonomyPickerState { export interface ITermChanges { changedCallback: (term: ITerm, checked: boolean) => void; activeNodes?: IPickerTerms; + disabledTermIds?: string[]; } diff --git a/src/controls/taxonomyPicker/TaxonomyPicker.tsx b/src/controls/taxonomyPicker/TaxonomyPicker.tsx index 1dce34d85..36baeaf90 100644 --- a/src/controls/taxonomyPicker/TaxonomyPicker.tsx +++ b/src/controls/taxonomyPicker/TaxonomyPicker.tsx @@ -1,14 +1,11 @@ import * as React from 'react'; -import { IWebPartContext } from '@microsoft/sp-webpart-base'; -import { PrimaryButton, DefaultButton, IconButton, IButtonProps } from 'office-ui-fabric-react/lib/Button'; +import { PrimaryButton, DefaultButton, IconButton } from 'office-ui-fabric-react/lib/Button'; import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel'; import { Spinner, SpinnerType } from 'office-ui-fabric-react/lib/Spinner'; -import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http'; import { Label } from 'office-ui-fabric-react/lib/Label'; import TermPicker from './TermPicker'; -import { BasePicker, IBasePickerProps, IPickerItemProps } from 'office-ui-fabric-react/lib/Pickers'; import { IPickerTerms, IPickerTerm } from './ITermPicker'; -import { ITaxonomyPickerProps, ITaxonomyPickerState, ITermParentProps, ITermParentState, ITermProps, ITermState } from './ITaxonomyPicker'; +import { ITaxonomyPickerProps, ITaxonomyPickerState } from './ITaxonomyPicker'; import SPTermStorePickerService from './../../services/SPTermStorePickerService'; import { ITermSet, IGroup, ITerm } from './../../services/ISPTermStorePickerService'; import styles from './TaxonomyPicker.module.scss'; @@ -31,7 +28,6 @@ export const TERM_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQC * Renders the controls for PropertyFieldTermPicker component */ export class TaxonomyPicker extends React.Component { - private delayedValidate: (value: IPickerTerms) => void; private termsService: SPTermStorePickerService; private previousValues: IPickerTerms = []; private cancel: boolean = true; @@ -244,7 +240,7 @@ export class TaxonomyPicker extends React.Component + disabledTermIds={this.props.disabledTermIds} /> @@ -286,6 +282,7 @@ export class TaxonomyPicker extends React.Component
diff --git a/src/controls/taxonomyPicker/Term.tsx b/src/controls/taxonomyPicker/Term.tsx index 66d8a396e..f0a109684 100644 --- a/src/controls/taxonomyPicker/Term.tsx +++ b/src/controls/taxonomyPicker/Term.tsx @@ -48,7 +48,22 @@ export default class Term extends React.Component { } } + /** + * Check if the current term needs to be disabled + */ + private checkIfTermIsDisabled(): boolean { + // Check if disabled term IDs are provided + if (this.props.disabledTermIds && this.props.disabledTermIds.length > 0) { + // Check if the current term ID exists in the disabled term IDs array + return this.props.disabledTermIds.indexOf(this.props.term.Id) !== -1; + } + return false; + } + + /** + * Default React render + */ public render(): JSX.Element { const styleProps: React.CSSProperties = { marginLeft: `${(this.props.term.PathDepth * 30)}px` @@ -58,7 +73,7 @@ export default class Term extends React.Component {
diff --git a/src/controls/taxonomyPicker/TermParent.tsx b/src/controls/taxonomyPicker/TermParent.tsx index 1cd044ae8..1b2dc007b 100644 --- a/src/controls/taxonomyPicker/TermParent.tsx +++ b/src/controls/taxonomyPicker/TermParent.tsx @@ -73,6 +73,9 @@ export default class TermParent extends React.Component; - // Check if the terms have been loaded - if (this.state.loaded) { - if (this._terms.length > 0) { - termElm = ( -
- { - this._terms.map(term => { - return ; - }) - } -
- ); - } else { - termElm =
{strings.TaxonomyPickerNoTerms}
; - } + // Check if the terms have been loaded + if (this.state.loaded) { + if (this._terms.length > 0) { + termElm = ( +
+ { + this._terms.map(term => { + return ; + }) + } +
+ ); } else { - termElm = ; + termElm =
{strings.TaxonomyPickerNoTerms}
; } + } else { + termElm = ; + } return ( diff --git a/src/controls/taxonomyPicker/TermPicker.tsx b/src/controls/taxonomyPicker/TermPicker.tsx index 0d486a83b..4f95cb7f8 100644 --- a/src/controls/taxonomyPicker/TermPicker.tsx +++ b/src/controls/taxonomyPicker/TermPicker.tsx @@ -25,6 +25,8 @@ export interface ITermPickerProps { value: IPickerTerms; allowMultipleSelections : boolean; isTermSetSelectable?: boolean; + disabledTermIds?: string[]; + onChanged: (items: IPickerTerm[]) => void; } @@ -83,7 +85,7 @@ export default class TermPicker extends React.Component 0 && this.props.disabledTermIds.indexOf(term.key) !== -1) { + break; + } + // Only retrieve the terms which are not yet tagged if (tagList.filter(tag => tag.key === term.key).length === 0) { filteredTerms.push(term); } diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index cf1ad2c1d..7eba0e3fc 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -221,8 +221,9 @@ export default class ControlsTest extends React.ComponentTaxonomyPicker tester: Date: Fri, 8 Jun 2018 11:32:17 +0200 Subject: [PATCH 06/15] Added property to disable children #82 --- CHANGELOG.md | 2 +- .../documentation/docs/about/release-notes.md | 2 +- .../docs/controls/TaxonomyPicker.md | 1 + .../taxonomyPicker/ITaxonomyPicker.ts | 6 +++ .../taxonomyPicker/TaxonomyPicker.tsx | 4 +- src/controls/taxonomyPicker/Term.tsx | 15 +------ src/controls/taxonomyPicker/TermParent.tsx | 22 +++++++++- src/controls/taxonomyPicker/TermPicker.tsx | 44 ++++++++++++++++--- .../controlsTest/components/ControlsTest.tsx | 5 ++- 9 files changed, 76 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ea59ea0..2f4ec55e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Enhancements** -- Added a property to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) +- Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) ## 1.4.0 diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index e3ea59ea0..2f4ec55e0 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -4,7 +4,7 @@ **Enhancements** -- Added a property to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) +- Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) ## 1.4.0 diff --git a/docs/documentation/docs/controls/TaxonomyPicker.md b/docs/documentation/docs/controls/TaxonomyPicker.md index 3b5866c7e..ac6b12625 100644 --- a/docs/documentation/docs/controls/TaxonomyPicker.md +++ b/docs/documentation/docs/controls/TaxonomyPicker.md @@ -69,6 +69,7 @@ The TaxonomyPicker control can be configured with the following properties: | onChange | function | no | captures the event of when the terms in the picker has changed. | | isTermSetSelectable | boolean | no | Specify if the TermSet itself is selectable in the tree view. | | disabledTermIds | string[] | no | Specify which terms should be disabled in the term set so that they cannot be selected. | +| disableChildrenOfDisabledParents | boolean | no | Specify if you want to disable the child terms when their parent is disabled. | | anchorId | string | no | Set the anchorid to a child term in the TermSet to be able to select terms from that level and below. | Interface `IPickerTerm` diff --git a/src/controls/taxonomyPicker/ITaxonomyPicker.ts b/src/controls/taxonomyPicker/ITaxonomyPicker.ts index 1c93744fe..f9d4a06e1 100644 --- a/src/controls/taxonomyPicker/ITaxonomyPicker.ts +++ b/src/controls/taxonomyPicker/ITaxonomyPicker.ts @@ -44,6 +44,10 @@ export interface ITaxonomyPickerProps { * Specify which terms should be disabled in the term set so that they cannot be selected */ disabledTermIds?: string[]; + /** + * Specify if you want to disable the child terms when their parent is disabled + */ + disableChildrenOfDisabledParents?: boolean; /** * Whether the property pane field is enabled or not. */ @@ -85,6 +89,7 @@ export interface ITermChanges { changedCallback: (term: ITerm, checked: boolean) => void; activeNodes?: IPickerTerms; disabledTermIds?: string[]; + disableChildrenOfDisabledParents?: boolean; } @@ -108,6 +113,7 @@ export interface ITermProps extends ITermChanges { termset: string; term: ITerm; multiSelection: boolean; + disabled: boolean; } export interface ITermState { diff --git a/src/controls/taxonomyPicker/TaxonomyPicker.tsx b/src/controls/taxonomyPicker/TaxonomyPicker.tsx index 36baeaf90..86d4e0677 100644 --- a/src/controls/taxonomyPicker/TaxonomyPicker.tsx +++ b/src/controls/taxonomyPicker/TaxonomyPicker.tsx @@ -240,7 +240,8 @@ export class TaxonomyPicker extends React.Component + disabledTermIds={this.props.disabledTermIds} + disableChildrenOfDisabledParents={this.props.disableChildrenOfDisabledParents} /> @@ -283,6 +284,7 @@ export class TaxonomyPicker extends React.Component
diff --git a/src/controls/taxonomyPicker/Term.tsx b/src/controls/taxonomyPicker/Term.tsx index f0a109684..977975cad 100644 --- a/src/controls/taxonomyPicker/Term.tsx +++ b/src/controls/taxonomyPicker/Term.tsx @@ -48,19 +48,6 @@ export default class Term extends React.Component { } } - /** - * Check if the current term needs to be disabled - */ - private checkIfTermIsDisabled(): boolean { - // Check if disabled term IDs are provided - if (this.props.disabledTermIds && this.props.disabledTermIds.length > 0) { - // Check if the current term ID exists in the disabled term IDs array - return this.props.disabledTermIds.indexOf(this.props.term.Id) !== -1; - } - - return false; - } - /** * Default React render */ @@ -73,7 +60,7 @@ export default class Term extends React.Component {
diff --git a/src/controls/taxonomyPicker/TermParent.tsx b/src/controls/taxonomyPicker/TermParent.tsx index 1b2dc007b..3691c271d 100644 --- a/src/controls/taxonomyPicker/TermParent.tsx +++ b/src/controls/taxonomyPicker/TermParent.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Spinner, SpinnerType } from 'office-ui-fabric-react/lib/Spinner'; import { ITermParentProps, ITermParentState } from './ITaxonomyPicker'; -import { ITerm, ITermSet } from '../../services/ISPTermStorePickerService'; +import { ITerm } from '../../services/ISPTermStorePickerService'; import { EXPANDED_IMG, COLLAPSED_IMG, TERMSET_IMG, TERM_IMG } from './TaxonomyPicker'; import Term from './Term'; @@ -87,11 +87,29 @@ export default class TermParent extends React.Component 0) { + let disabledPaths = []; termElm = (
{ this._terms.map(term => { - return ; + // debugger; + let disabled = false; + if (this.props.disabledTermIds && this.props.disabledTermIds.length > 0) { + // Check if the current term ID exists in the disabled term IDs array + disabled = this.props.disabledTermIds.indexOf(term.Id) !== -1; + if (disabled) { + // Push paths to the disabled list + disabledPaths.push(term.PathOfTerm); + } + } + + if (this.props.disableChildrenOfDisabledParents) { + // Check if parent is disabled + const parentPath = disabledPaths.filter(p => term.PathOfTerm.indexOf(p) !== -1); + disabled = parentPath && parentPath.length > 0; + } + + return ; }) }
diff --git a/src/controls/taxonomyPicker/TermPicker.tsx b/src/controls/taxonomyPicker/TermPicker.tsx index 4f95cb7f8..2b223e071 100644 --- a/src/controls/taxonomyPicker/TermPicker.tsx +++ b/src/controls/taxonomyPicker/TermPicker.tsx @@ -8,6 +8,7 @@ import { IWebPartContext } from '@microsoft/sp-webpart-base'; import * as strings from 'ControlStrings'; import { Icon } from 'office-ui-fabric-react'; import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; +import { ITermSet } from '../../services/ISPTermStorePickerService'; export class TermBasePicker extends BasePicker> { @@ -26,11 +27,13 @@ export interface ITermPickerProps { allowMultipleSelections : boolean; isTermSetSelectable?: boolean; disabledTermIds?: string[]; + disableChildrenOfDisabledParents?: boolean; onChanged: (items: IPickerTerm[]) => void; } export default class TermPicker extends React.Component { + private allTerms: ITermSet = null; /** * Constructor method @@ -126,14 +129,45 @@ export default class TermPicker extends React.Component 0 && this.props.disabledTermIds.indexOf(term.key) !== -1) { - break; + if (disabledTermIds && disabledTermIds.length > 0) { + // Check if current term need to be disabled + if (disabledTermIds.indexOf(term.key) !== -1) { + canBePicked = false; + } else { + // Check if child terms need to be disabled + if (disableChildrenOfDisabledParents) { + // Check if terms were already retrieved + if (!this.allTerms) { + this.allTerms = await termsService.getAllTerms(this.props.termPickerHostProps.termsetNameOrID); + } + + // Check if there are terms retrieved + if (this.allTerms.Terms && this.allTerms.Terms.length > 0) { + // Find the disabled parents + const disabledParents = this.allTerms.Terms.filter(t => disabledTermIds.indexOf(t.Id) !== -1); + // Check if disabled parents were found + if (disabledParents && disabledParents.length > 0) { + // Check if the current term lives underneath a disabled parent + const findTerm = disabledParents.filter(pt => term.path.indexOf(pt.PathOfTerm) !== -1); + if (findTerm && findTerm.length > 0) { + canBePicked = false; + } + } + } + } + } } - // Only retrieve the terms which are not yet tagged - if (tagList.filter(tag => tag.key === term.key).length === 0) { - filteredTerms.push(term); + + if (canBePicked) { + // Only retrieve the terms which are not yet tagged + if (tagList.filter(tag => tag.key === term.key).length === 0) { + filteredTerms.push(term); + } } } return filteredTerms; diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 7eba0e3fc..3fb8de8c0 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -223,7 +223,10 @@ export default class ControlsTest extends React.Component Date: Tue, 12 Jun 2018 16:57:35 +0200 Subject: [PATCH 07/15] Updated change log process --- CHANGELOG.JSON | 183 ++++++++++++++++++ CHANGELOG.md | 43 +++- .../documentation/docs/about/release-notes.md | 43 +++- package.json | 2 +- scripts/create-changelog.js | 37 ++++ src/controls/taxonomyPicker/TermParent.tsx | 1 - .../controlsTest/components/ControlsTest.tsx | 2 +- 7 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 CHANGELOG.JSON create mode 100644 scripts/create-changelog.js diff --git a/CHANGELOG.JSON b/CHANGELOG.JSON new file mode 100644 index 000000000..720040700 --- /dev/null +++ b/CHANGELOG.JSON @@ -0,0 +1,183 @@ +{ + "versions": [ + { + "version": "1.5.0", + "changes": { + "new": [], + "enhancements": [ + "Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82)" + ], + "fixes": [] + }, + "contributions": [] + }, + { + "version": "1.4.0", + "changes": { + "new": [ + "`SecurityTrimmedControl` control got added [#74](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/74)" + ], + "enhancements": [ + "Allow the `TaxonomyPicker` to also be used in Application Customizer [#77](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/77)", + "Add `npm postinstall` script to automatically add the locale config [#78](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/78)" + ], + "fixes": [ + "Icon not showing up in the `Placeholder` control [#76](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/76)" + ] + }, + "contributions": [] + }, + { + "version": "1.3.0", + "changes": { + "new": [], + "enhancements": [ + "`TaxonomyPicker` control got added [#22](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/22) [#63](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/63) [#64](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/64)", + "`ListPicker` control got added [#34](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/34)" + ], + "fixes": [ + "Issue fixed when the optional `selection` property was not provided to the `ListView` [#65](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/65)" + ] + }, + "contributions": [] + }, + { + "version": "1.2.5", + "changes": { + "new": [], + "enhancements": [], + "fixes": [ + "Undo `ListView` item selection after items array updates [#55](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/55)" + ] + }, + "contributions": [] + }, + { + "version": "1.2.4", + "changes": { + "new": [], + "enhancements": [ + "Hiding placeholder title on small zones" + ], + "fixes": [ + "iFrame dialog reference fix [#52](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/52)" + ] + }, + "contributions": [] + }, + { + "version": "1.2.3", + "changes": { + "new": [], + "enhancements": [ + "Optimized telemetry so that it only pushes control data", + "`WebPartTitle` hide control completely when empty" + ], + "fixes": [] + }, + "contributions": [] + }, + { + "version": "1.2.2", + "changes": { + "new": [], + "enhancements": [], + "fixes": ["Fixes an issue sorting in the `ListView` control while items were selected. Indexes were not updated."] + }, + "contributions": [] + }, + { + "version": "1.2.1", + "changes": { + "new": [], + "enhancements": [], + "fixes": ["`FieldTaxonomyRenderer` got fixed to support single and multiple values"] + }, + "contributions": [] + }, + { + "version": "1.2.0", + "changes": { + "new": [ + "Field controls are added to the project", + "`IFrameDialog` was added to the project" + ], + "enhancements": [], + "fixes": ["Fixed theming in the `WebPartTitle` control"] + }, + "contributions": [] + }, + { + "version": "1.1.3", + "changes": { + "new": [], + "enhancements": [], + "fixes": ["`FileTypeIcon` icon fixed where it did not render an icon. This control should now works in SPFx extensions."] + }, + "contributions": [] + }, + { + "version": "1.1.2", + "changes": { + "new": [], + "enhancements": ["Improved telemetry with some object checks"], + "fixes": ["Fix for `WebPartTitle` control to inherit color"] + }, + "contributions": [] + }, + { + "version": "1.1.1", + "changes": { + "new": [], + "enhancements": ["Removed operation name from telemetry"], + "fixes": [] + }, + "contributions": [] + }, + { + "version": "1.1.0", + "changes": { + "new": [], + "enhancements": ["Telemetry added"], + "fixes": [] + }, + "contributions": [] + }, + { + "version": "1.0.0", + "changes": { + "new": ["`WebPartTitle` control got added"], + "enhancements": ["ListView control got extended with the ability to specify a set of preselected items."], + "fixes": [] + }, + "contributions": [] + }, + { + "version": "Beta 1.0.0-beta.8", + "changes": { + "new": [], + "enhancements": [], + "fixes": ["Fix for the `ListView` control when selection is used in combination with `setState`."] + }, + "contributions": [] + }, + { + "version": "Beta 1.0.0-beta.7", + "changes": { + "new": ["Grouping functionality added to the `ListView` control"], + "enhancements": [], + "fixes": [] + }, + "contributions": [] + }, + { + "version": "Beta 1.0.0-beta.6", + "changes": { + "new": ["Initial release"], + "enhancements": [], + "fixes": [] + }, + "contributions": [] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f4ec55e0..19a5d73dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ## 1.4.0 -**New Controls** +**New control(s)** - `SecurityTrimmedControl` control got added [#74](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/74) @@ -23,7 +23,7 @@ ## 1.3.0 -**New Controls** +**Enhancements** - `TaxonomyPicker` control got added [#22](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/22) [#63](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/63) [#64](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/64) - `ListPicker` control got added [#34](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/34) @@ -69,7 +69,7 @@ ## 1.2.0 -**New controls** +**New control(s)** - Field controls are added to the project - `IFrameDialog` was added to the project @@ -80,31 +80,56 @@ ## 1.1.3 -- `FileTypeIcon` icon fixed. This control should now also work in SPFx extensions. +**Fixes** + +- `FileTypeIcon` icon fixed where it did not render an icon. This control should now works in SPFx extensions. ## 1.1.2 -- Fix for `WebPartTitle` control to inherit color +**Enhancements** + - Improved telemetry with some object checks +**Fixes** + +- Fix for `WebPartTitle` control to inherit color + ## 1.1.1 +**Enhancements** + - Removed operation name from telemetry ## 1.1.0 +**Enhancements** + - Telemetry added ## 1.0.0 -- **New control**: WebPartTitle control got added. -- **Enhancement**: ListView control got extended with the ability to specify a set of preselected items. + +**New control(s)** + +- `WebPartTitle` control got added + +**Enhancements** + +- ListView control got extended with the ability to specify a set of preselected items. ## Beta 1.0.0-beta.8 -- **Bug fix**: bug fix for the `ListView` control when selection is used in combination with `setState`. + +**Fixes** + +- Fix for the `ListView` control when selection is used in combination with `setState`. ## Beta 1.0.0-beta.7 -**Added** + +**New control(s)** + - Grouping functionality added to the `ListView` control ## Beta 1.0.0-beta.6 + +**New control(s)** + - Initial release diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index 2f4ec55e0..19a5d73dd 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -8,7 +8,7 @@ ## 1.4.0 -**New Controls** +**New control(s)** - `SecurityTrimmedControl` control got added [#74](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/74) @@ -23,7 +23,7 @@ ## 1.3.0 -**New Controls** +**Enhancements** - `TaxonomyPicker` control got added [#22](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/22) [#63](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/63) [#64](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/64) - `ListPicker` control got added [#34](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/34) @@ -69,7 +69,7 @@ ## 1.2.0 -**New controls** +**New control(s)** - Field controls are added to the project - `IFrameDialog` was added to the project @@ -80,31 +80,56 @@ ## 1.1.3 -- `FileTypeIcon` icon fixed. This control should now also work in SPFx extensions. +**Fixes** + +- `FileTypeIcon` icon fixed where it did not render an icon. This control should now works in SPFx extensions. ## 1.1.2 -- Fix for `WebPartTitle` control to inherit color +**Enhancements** + - Improved telemetry with some object checks +**Fixes** + +- Fix for `WebPartTitle` control to inherit color + ## 1.1.1 +**Enhancements** + - Removed operation name from telemetry ## 1.1.0 +**Enhancements** + - Telemetry added ## 1.0.0 -- **New control**: WebPartTitle control got added. -- **Enhancement**: ListView control got extended with the ability to specify a set of preselected items. + +**New control(s)** + +- `WebPartTitle` control got added + +**Enhancements** + +- ListView control got extended with the ability to specify a set of preselected items. ## Beta 1.0.0-beta.8 -- **Bug fix**: bug fix for the `ListView` control when selection is used in combination with `setState`. + +**Fixes** + +- Fix for the `ListView` control when selection is used in combination with `setState`. ## Beta 1.0.0-beta.7 -**Added** + +**New control(s)** + - Grouping functionality added to the `ListView` control ## Beta 1.0.0-beta.6 + +**New control(s)** + - Initial release diff --git a/package.json b/package.json index b6b99824d..593bb4ead 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "prepublishOnly": "gulp", "versionUpdater": "gulp versionUpdater", "karma": "karma start --circle true", - "changelog": "node scripts/sync-changelogs.js", + "changelog": "node scripts/create-changelog.js && node scripts/sync-changelogs.js", "postinstall": "node postinstall/install.js" }, "dependencies": { diff --git a/scripts/create-changelog.js b/scripts/create-changelog.js new file mode 100644 index 000000000..9054b11d6 --- /dev/null +++ b/scripts/create-changelog.js @@ -0,0 +1,37 @@ +const changelog = require('../CHANGELOG.json'); +const fs = require('fs'); + +if (changelog.versions && changelog.versions.length > 0) { + const markdown = []; + + markdown.push(`# Releases`); + markdown.push(``); + + // Loop over all the change log versions + for (const entry of changelog.versions) { + markdown.push(`## ${entry.version}`); + markdown.push(``); + // Check if entry contains change information + if (entry.changes) { + // Loop over all change types + for (const changeName in entry.changes) { + const typeChanges = entry.changes[changeName]; + if (typeChanges.length > 0) { + let name = changeName === "new" ? "new control(s)" : changeName; + markdown.push(`**${name.charAt(0).toUpperCase() + name.slice(1)}**`); + markdown.push(``); + + // Add each change text + for (const msg of typeChanges) { + markdown.push(`- ${msg}`); + } + markdown.push(``); + } + } + } + } + + if (markdown.length > 2) { + fs.writeFileSync('CHANGELOG.md', markdown.join('\n')); + } +} diff --git a/src/controls/taxonomyPicker/TermParent.tsx b/src/controls/taxonomyPicker/TermParent.tsx index 3691c271d..4a6f8a18a 100644 --- a/src/controls/taxonomyPicker/TermParent.tsx +++ b/src/controls/taxonomyPicker/TermParent.tsx @@ -92,7 +92,6 @@ export default class TermParent extends React.Component { this._terms.map(term => { - // debugger; let disabled = false; if (this.props.disabledTermIds && this.props.disabledTermIds.length > 0) { // Check if the current term ID exists in the disabled term IDs array diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 3fb8de8c0..28fa03b9a 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -226,7 +226,7 @@ export default class ControlsTest extends React.Component Date: Tue, 5 Jun 2018 22:24:08 +1000 Subject: [PATCH 08/15] Updated show all users when groupName is empty, added mkdocs and commented console logs --- .../assets/Peoplepicker-multiplechoices.png | Bin 0 -> 4112 bytes .../assets/Peoplepicker-selectingchoices.png | Bin 0 -> 20942 bytes .../Peoplepicker-witherrorandtooltip.png | Bin 0 -> 7824 bytes .../docs/controls/PeoplePicker.md | 70 ++++++++++++++++++ src/controls/peoplepicker/IPeoplePicker.ts | 6 +- .../peoplepicker/PeoplePickerComponent.tsx | 14 ++-- .../controlsTest/components/ControlsTest.tsx | 1 - 7 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 docs/documentation/docs/assets/Peoplepicker-multiplechoices.png create mode 100644 docs/documentation/docs/assets/Peoplepicker-selectingchoices.png create mode 100644 docs/documentation/docs/assets/Peoplepicker-witherrorandtooltip.png create mode 100644 docs/documentation/docs/controls/PeoplePicker.md diff --git a/docs/documentation/docs/assets/Peoplepicker-multiplechoices.png b/docs/documentation/docs/assets/Peoplepicker-multiplechoices.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c4ff4f17dc3fc6cc62783112e8fde92f6a079e GIT binary patch literal 4112 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 literal 0 HcmV?d00001 diff --git a/docs/documentation/docs/assets/Peoplepicker-selectingchoices.png b/docs/documentation/docs/assets/Peoplepicker-selectingchoices.png new file mode 100644 index 0000000000000000000000000000000000000000..732a5071501faa3749f4c7b17eb5ee622d7eb284 GIT binary patch literal 20942 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 literal 0 HcmV?d00001 diff --git a/docs/documentation/docs/assets/Peoplepicker-witherrorandtooltip.png b/docs/documentation/docs/assets/Peoplepicker-witherrorandtooltip.png new file mode 100644 index 0000000000000000000000000000000000000000..5bbc2471547e6179c9909307b702adbf1dc22192 GIT binary patch literal 7824 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 KHdNE`#s31qbTiTb literal 0 HcmV?d00001 diff --git a/docs/documentation/docs/controls/PeoplePicker.md b/docs/documentation/docs/controls/PeoplePicker.md new file mode 100644 index 000000000..67be4322c --- /dev/null +++ b/docs/documentation/docs/controls/PeoplePicker.md @@ -0,0 +1,70 @@ +# People Picker + +This control renders a People picker field which can be used to select one or many users from a SharePoint group, or filter from all users in a SharePoint site. You could also set the control as mandatory and show a custom error message if field is empty. + +**Empty People Picker control with error message and tooltip** + +![People Picker](../assets/Peoplepicker-witherrorandtooltip.png) + +**Selecting People** + +![Selecting People](../assets/Peoplepicker-selectingchoices.png) + +**Selected people** + +![Selected people](../assets/Peoplepicker-multiplechoices.png) + + +## How to use this control in your solutions + +- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../#getting-started) page for more information about installing the dependency. +- Import the following modules to your component: + +```TypeScript +import { PeoplePicker } from "@pnp/spfx-controls-react/lib/PeoplePicker"; +``` + +- Use the `PeoplePicker` control in your code as follows: + +```TypeScript + +``` + +- With the `selectedItems` property you can get the selected People in the Peoplepicker : + +```typescript +private _getPeoplePickerItems(items: any[]) { + console.log('Items:', items); + } +``` + +## Implementation + +The People picker control can be configured with the following properties: + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| context | WebPartContext | yes | Context of the current web part. | +| groupName | string | yes | group from which users are fetched. Leave it blank if need to filter all users | +| titleText | string | yes | Text to be displayed on the control | +| personSelectionLimit | number | no | Defines the limit of people that can be selected in the control| +| isRequired | boolean | no | Set if the control is required or not | +| errorMessage | string | no | Specify the error message to display | +| errorMessageclassName | string | no | applies custom styling to the error message section| +| showtooltip | boolean | no | Defines if need a tooltip or not | +| tooltip | string | no | Specify the tooltip message to display | +| tooltipDirectional | DirectionalHint | no | Direction where the tooltip would be shown | +| selectedItems | function | no | get the selected users in the control| +| peoplePickerWPclassName | string | no | applies custom styling to the people picker element| +| peoplePickerCntrlclassName | string | no | applies custom styling to the people picker control only| + + +![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/PeoplePicker) diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts index 2d13982e5..c187a5f94 100644 --- a/src/controls/peoplepicker/IPeoplePicker.ts +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -13,11 +13,7 @@ export interface IPeoplePickerProps { /** * Name of SharePoint Group */ - groupName?: string; - /** - * image Initials - */ - getAllUsers?: boolean; + groupName: string; /** * Text of the Control */ diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index 1ac78e75f..58e7fd471 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -29,7 +29,6 @@ export class PeoplePicker extends React.Component { removeButtonAriaLabel={ 'Remove' } inputProps={ { 'aria-label': 'People Picker', - onBlur: (ev: React.FocusEvent) => console.log('onBlur on People Picker called'), - onFocus: (ev: React.FocusEvent) => console.log('onFocus on People Picker called'), + //onBlur: (ev: React.FocusEvent) => console.log('onBlur on People Picker called'), + //onFocus: (ev: React.FocusEvent) => console.log('onFocus on People Picker called'), } } itemLimit={this.props.personSelectionLimit} onChange = { this._onPersonItemsChange } diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index e3173ff47..b1b61afa2 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -284,7 +284,6 @@ export default class ControlsTest extends React.Component Date: Wed, 13 Jun 2018 17:59:34 +0200 Subject: [PATCH 09/15] Fix issue for #83 --- CHANGELOG.JSON | 4 +++- .../taxonomyPicker/TaxonomyPicker.tsx | 20 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.JSON b/CHANGELOG.JSON index 720040700..2068f4b96 100644 --- a/CHANGELOG.JSON +++ b/CHANGELOG.JSON @@ -7,7 +7,9 @@ "enhancements": [ "Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82)" ], - "fixes": [] + "fixes": [ + "Bug in TaxonomyPicker where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83)" + ] }, "contributions": [] }, diff --git a/src/controls/taxonomyPicker/TaxonomyPicker.tsx b/src/controls/taxonomyPicker/TaxonomyPicker.tsx index 86d4e0677..f72d80ac2 100644 --- a/src/controls/taxonomyPicker/TaxonomyPicker.tsx +++ b/src/controls/taxonomyPicker/TaxonomyPicker.tsx @@ -41,7 +41,7 @@ export class TaxonomyPicker extends React.Component Date: Wed, 13 Jun 2018 18:13:22 +0200 Subject: [PATCH 10/15] Updated trigger --- CHANGELOG.JSON | 2 +- CHANGELOG.md | 4 ++++ docs/documentation/docs/about/release-notes.md | 4 ++++ src/controls/taxonomyPicker/TaxonomyPicker.tsx | 12 +++++++----- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.JSON b/CHANGELOG.JSON index 2068f4b96..398507ab7 100644 --- a/CHANGELOG.JSON +++ b/CHANGELOG.JSON @@ -8,7 +8,7 @@ "Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82)" ], "fixes": [ - "Bug in TaxonomyPicker where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83)" + "Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83)" ] }, "contributions": [] diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a5d73dd..119359b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) +**Fixes** + +- Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) + ## 1.4.0 **New control(s)** diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index 19a5d73dd..119359b04 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -6,6 +6,10 @@ - Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) +**Fixes** + +- Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) + ## 1.4.0 **New control(s)** diff --git a/src/controls/taxonomyPicker/TaxonomyPicker.tsx b/src/controls/taxonomyPicker/TaxonomyPicker.tsx index f72d80ac2..cc2605e26 100644 --- a/src/controls/taxonomyPicker/TaxonomyPicker.tsx +++ b/src/controls/taxonomyPicker/TaxonomyPicker.tsx @@ -9,7 +9,7 @@ import { ITaxonomyPickerProps, ITaxonomyPickerState } from './ITaxonomyPicker'; import SPTermStorePickerService from './../../services/SPTermStorePickerService'; import { ITermSet, IGroup, ITerm } from './../../services/ISPTermStorePickerService'; import styles from './TaxonomyPicker.module.scss'; -import { sortBy, uniqBy, cloneDeep } from '@microsoft/sp-lodash-subset'; +import { sortBy, uniqBy, cloneDeep, isEqual } from '@microsoft/sp-lodash-subset'; import TermParent from './TermParent'; import FieldErrorMessage from './ErrorMessage'; import * as appInsights from '../../common/appInsights'; @@ -67,10 +67,12 @@ export class TaxonomyPicker extends React.Component Date: Wed, 13 Jun 2018 21:42:33 +0200 Subject: [PATCH 11/15] Check if initial values are not equal --- .../taxonomyPicker/TaxonomyPicker.tsx | 3 ++- .../controlsTest/components/ControlsTest.tsx | 24 +++++++++++++++---- .../components/IControlsTestProps.ts | 1 + 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/controls/taxonomyPicker/TaxonomyPicker.tsx b/src/controls/taxonomyPicker/TaxonomyPicker.tsx index cc2605e26..9bd43f234 100644 --- a/src/controls/taxonomyPicker/TaxonomyPicker.tsx +++ b/src/controls/taxonomyPicker/TaxonomyPicker.tsx @@ -68,7 +68,8 @@ export class TaxonomyPicker extends React.Component { + this.setState({ + initialValues: terms + }); + console.log("Terms:", terms); } /** @@ -220,6 +224,7 @@ export default class ControlsTest extends React.ComponentTaxonomyPicker tester: + + { + this.setState({ + initialValues: [{ + key: "ab703558-2546-4b23-b8b8-2bcb2c0086f5", + name: "HR", + path: "HR", + termSet: "b3e9b754-2593-4ae6-abc2-35345402e186" + }] + }); + }} />
iframe dialog tester: Date: Fri, 15 Jun 2018 15:23:15 +0300 Subject: [PATCH 12/15] Fix of #84 - additional request to get Login Name for the user --- src/common/utilities/SPHelper.ts | 34 ++++++ .../fieldUserRenderer/FieldUserRenderer.tsx | 103 +++++++++--------- 2 files changed, 83 insertions(+), 54 deletions(-) diff --git a/src/common/utilities/SPHelper.ts b/src/common/utilities/SPHelper.ts index e995a2e1a..d8fb58cc7 100644 --- a/src/common/utilities/SPHelper.ts +++ b/src/common/utilities/SPHelper.ts @@ -5,6 +5,7 @@ import * as Constants from '../Constants'; import { ListItemAccessor } from '@microsoft/sp-listview-extensibility'; import { SPField } from '@microsoft/sp-page-context'; import { sp } from '@pnp/sp'; +import { SPHttpClient } from '@microsoft/sp-http'; declare var window: any; @@ -328,6 +329,39 @@ export class SPHelper { return viewIdQueryParam || context.pageContext.legacyPageContext.viewId; } + /** + * Returns the user corresponding to the specified member identifier for the current site + * @param id user id + * @param context SPFx context + */ + public static async getUserById(id: number, context: IContext): Promise { + sp.setup({ + spfxContext: context + }); + + return sp.web.getUserById(id).get(); + } + + /** + * Returns user profile properties + * @param loginName User's login name + * @param context SPFx context + */ + public static async getUserProperties(loginName: string, context: IContext): Promise { + let url: string; + url = context.pageContext.web.absoluteUrl; + url = GeneralHelper.trimSlash(url); + + url += `/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor('${encodeURIComponent(loginName)}')`; + return context.spHttpClient.get(url, SPHttpClient.configurations.v1) + .then((response): Promise => { + return response.json(); + }) + .then((value) => { + return value; + }); + } + private static _updateFieldInSessionStorage(field: ISPField, context: IContext): void { let loadedViewFields: { [viewId: string]: IFields } = SPHelper._getLoadedViewFieldsFromStorage(); diff --git a/src/controls/fields/fieldUserRenderer/FieldUserRenderer.tsx b/src/controls/fields/fieldUserRenderer/FieldUserRenderer.tsx index b897036f9..73d7fe597 100644 --- a/src/controls/fields/fieldUserRenderer/FieldUserRenderer.tsx +++ b/src/controls/fields/fieldUserRenderer/FieldUserRenderer.tsx @@ -15,6 +15,7 @@ import FieldUserHoverCard, { IFieldUserHoverCardProps } from './FieldUserHoverCa import * as appInsights from '../../../common/appInsights'; import * as strings from 'ControlStrings'; +import { SPHelper } from '../../../common/utilities'; export interface IFieldUserRendererProps extends IFieldRendererProps { /** @@ -173,18 +174,18 @@ export class FieldUserRenderer extends React.Component
{strings.Contact}
- + {user.email}
{user.workPhone &&
- + {user.workPhone}
} {user.cellPhone &&
- + {user.cellPhone}
} @@ -236,67 +237,61 @@ export class FieldUserRenderer extends React.Component { if (this._loadedUserProfiles[user.id]) { return; // we've already have the profile info } const context: IContext = this.props.context; - let url: string; - url = context.pageContext.web.absoluteUrl; - url = GeneralHelper.trimSlash(url); - url += `/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor('i%3A0%23.f%7Cmembership%7C${user.email.replace('@', '%40')}')`; - context.spHttpClient.get(url, SPHttpClient.configurations.v1) - .then((response): Promise => { - return response.json(); - }) - .then((value) => { - const mthumbStr = 'MThumb.jpg'; - const userProfileProps: IUserProfileProperties = { - displayName: value.DisplayName, - email: value.Email, - jobTitle: value.Title, - userUrl: value.UserUrl, - pictureUrl: value.PictureUrl && value.PictureUrl.toString().indexOf(mthumbStr) === value.PictureUrl.toString().length - mthumbStr.length ? '' : value.PictureUrl //this._userImageUrl.replace('{0}', user.email) - }; + const siteUser = await SPHelper.getUserById(parseInt(user.id), context); - const props: IODataKeyValuePair[] = value.UserProfileProperties as IODataKeyValuePair[]; - let foundPropsCount: number = 0; - for (let i = 0, len = props.length; i < len; i++) { - const prop: IODataKeyValuePair = props[i]; - switch (prop.Key) { - case 'WorkPhone': - userProfileProps.workPhone = prop.Value; - foundPropsCount++; - break; - case 'Department': - userProfileProps.department = prop.Value; - foundPropsCount++; - break; - case 'SPS-SipAddress': - userProfileProps.sip = prop.Value; - foundPropsCount++; - break; - case 'CellPhone': - userProfileProps.cellPhone = prop.Value; - foundPropsCount++; - break; - } + const value = await SPHelper.getUserProperties(siteUser.LoginName, context); - if (foundPropsCount === 4) { - break; - } - } + const mthumbStr = 'MThumb.jpg'; + const userProfileProps: IUserProfileProperties = { + displayName: value.DisplayName, + email: value.Email, + jobTitle: value.Title, + userUrl: value.UserUrl, + pictureUrl: value.PictureUrl && value.PictureUrl.toString().indexOf(mthumbStr) === value.PictureUrl.toString().length - mthumbStr.length ? '' : value.PictureUrl //this._userImageUrl.replace('{0}', user.email) + }; - this._loadedUserProfiles[user.id] = userProfileProps; - this.setState((prevState: IFieldUserRendererState, componentProps: IFieldUserRendererProps) => { - const newUsers = _.clone(prevState.users); - newUsers[index] = this._getUserFromPrincipalAndProps(this.props.users[index], userProfileProps); + const props: IODataKeyValuePair[] = value.UserProfileProperties as IODataKeyValuePair[]; + let foundPropsCount: number = 0; + for (let i = 0, len = props.length; i < len; i++) { + const prop: IODataKeyValuePair = props[i]; + switch (prop.Key) { + case 'WorkPhone': + userProfileProps.workPhone = prop.Value; + foundPropsCount++; + break; + case 'Department': + userProfileProps.department = prop.Value; + foundPropsCount++; + break; + case 'SPS-SipAddress': + userProfileProps.sip = prop.Value; + foundPropsCount++; + break; + case 'CellPhone': + userProfileProps.cellPhone = prop.Value; + foundPropsCount++; + break; + } - return { users: newUsers }; + if (foundPropsCount === 4) { + break; + } + } + + this._loadedUserProfiles[user.id] = userProfileProps; + this.setState((prevState: IFieldUserRendererState, componentProps: IFieldUserRendererProps) => { + const newUsers = _.clone(prevState.users); + newUsers[index] = this._getUserFromPrincipalAndProps(this.props.users[index], userProfileProps); - }); - }); + return { users: newUsers }; + + }); } } From 1ab6145a6ab4350b48b1d0c90374eaa26b844910 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 15 Jun 2018 14:41:23 +0200 Subject: [PATCH 13/15] Changelog update for #84 --- CHANGELOG.JSON | 3 ++- CHANGELOG.md | 1 + docs/documentation/docs/about/release-notes.md | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.JSON b/CHANGELOG.JSON index 398507ab7..d15a22b7e 100644 --- a/CHANGELOG.JSON +++ b/CHANGELOG.JSON @@ -8,7 +8,8 @@ "Added a properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82)" ], "fixes": [ - "Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83)" + "Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83)", + "`FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84)" ] }, "contributions": [] diff --git a/CHANGELOG.md b/CHANGELOG.md index 119359b04..dcd23c92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ **Fixes** - Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) +- `FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84) ## 1.4.0 diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index 119359b04..dcd23c92e 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -9,6 +9,7 @@ **Fixes** - Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) +- `FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84) ## 1.4.0 From c5f9603dbc659b5e0b0d008bb860e71e553313e0 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 15 Jun 2018 15:51:17 +0200 Subject: [PATCH 14/15] Code cleanup to PeoplePicker --- .../docs/controls/PeoplePicker.md | 21 +- src/controls/peoplepicker/IPeoplePicker.ts | 12 +- src/controls/peoplepicker/IUsers.ts | 25 ++ .../peoplepicker/PeoplePickerComponent.tsx | 398 +++++++++--------- src/loc/en-us.ts | 6 +- src/loc/mystrings.d.ts | 3 + .../controlsTest/components/ControlsTest.tsx | 21 +- 7 files changed, 269 insertions(+), 217 deletions(-) create mode 100644 src/controls/peoplepicker/IUsers.ts diff --git a/docs/documentation/docs/controls/PeoplePicker.md b/docs/documentation/docs/controls/PeoplePicker.md index 67be4322c..bb5aa6636 100644 --- a/docs/documentation/docs/controls/PeoplePicker.md +++ b/docs/documentation/docs/controls/PeoplePicker.md @@ -1,6 +1,6 @@ # People Picker -This control renders a People picker field which can be used to select one or many users from a SharePoint group, or filter from all users in a SharePoint site. You could also set the control as mandatory and show a custom error message if field is empty. +This control renders a People picker field which can be used to select one or more users from a SharePoint group or site. The control can be configured as mandatory. It will show a custom error message if field is empty. **Empty People Picker control with error message and tooltip** @@ -20,30 +20,29 @@ This control renders a People picker field which can be used to select one or ma - Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../#getting-started) page for more information about installing the dependency. - Import the following modules to your component: -```TypeScript +```typescript import { PeoplePicker } from "@pnp/spfx-controls-react/lib/PeoplePicker"; ``` - Use the `PeoplePicker` control in your code as follows: -```TypeScript +```typescript + groupName={"Team Site Owners"} // Leave this blank in case you want to filter from all users + showtooltip={true} + isRequired={true} + selectedItems={this._getPeoplePickerItems} /> ``` - With the `selectedItems` property you can get the selected People in the Peoplepicker : ```typescript private _getPeoplePickerItems(items: any[]) { - console.log('Items:', items); - } + console.log('Items:', items); +} ``` ## Implementation @@ -53,8 +52,8 @@ The People picker control can be configured with the following properties: | Property | Type | Required | Description | | ---- | ---- | ---- | ---- | | context | WebPartContext | yes | Context of the current web part. | -| groupName | string | yes | group from which users are fetched. Leave it blank if need to filter all users | | titleText | string | yes | Text to be displayed on the control | +| groupName | string | no | group from which users are fetched. Leave it blank if need to filter all users | | personSelectionLimit | number | no | Defines the limit of people that can be selected in the control| | isRequired | boolean | no | Set if the control is required or not | | errorMessage | string | no | Specify the error message to display | diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts index c187a5f94..1c6d6a5e1 100644 --- a/src/controls/peoplepicker/IPeoplePicker.ts +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -11,13 +11,13 @@ export interface IPeoplePickerProps { */ context: WebPartContext; /** - * Name of SharePoint Group - */ - groupName: string; - /** - * Text of the Control + * Text of the Control */ titleText: string; + /** + * Name of SharePoint Group + */ + groupName?: string; /** * Selection Limit of Control */ @@ -37,7 +37,7 @@ export interface IPeoplePickerProps { /** * Method to check value of People Picker text */ - selectedItems?: (items: any[]) => void; + selectedItems?: (items: any[]) => void; /** * Tooltip Message */ diff --git a/src/controls/peoplepicker/IUsers.ts b/src/controls/peoplepicker/IUsers.ts new file mode 100644 index 000000000..c3f52362d --- /dev/null +++ b/src/controls/peoplepicker/IUsers.ts @@ -0,0 +1,25 @@ +export interface IUsers { + '@odata.context': string; + value: Value[]; +} + +export interface Value { + '@odata.type': string; + '@odata.id': string; + '@odata.editLink': string; + Id: number; + IsHiddenInUI: boolean; + LoginName: string; + Title: string; + PrincipalType: number; + Email: string; + IsEmailAuthenticationGuestUser: boolean; + IsShareByEmailGuestUser: boolean; + IsSiteAdmin: boolean; + UserId?: UserId; +} + +export interface UserId { + NameId: string; + NameIdIssuer: string; +} diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index 58e7fd471..8fbc0fe10 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -1,12 +1,12 @@ +import * as strings from 'ControlStrings'; import * as React from 'react'; import { IPeoplePickerProps, IPeoplePickerState } from './IPeoplePicker'; -import { Persona, IPersonaProps } from 'office-ui-fabric-react/lib/Persona'; +import { IPersonaProps } from 'office-ui-fabric-react/lib/Persona'; import { TooltipHost, DirectionalHint } from 'office-ui-fabric-react/lib/Tooltip'; import { IBasePickerSuggestionsProps } from 'office-ui-fabric-react/lib/Pickers'; import { NormalPeoplePicker } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePicker'; import { IPersonaWithMenu } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePickerItems/PeoplePickerItem.Props'; import { ValidationState } from 'office-ui-fabric-react/lib/Pickers'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; import { SPHttpClient } from '@microsoft/sp-http'; import styles from './PeoplePickerComponent.module.scss'; @@ -14,7 +14,8 @@ import * as appInsights from '../../common/appInsights'; import { assign } from 'office-ui-fabric-react/lib/Utilities'; -import { autobind } from 'office-ui-fabric-react'; +import { IUsers } from './IUsers'; +import { Label } from 'office-ui-fabric-react/lib/Label'; const suggestionProps: IBasePickerSuggestionsProps = { suggestionsHeaderText: 'Suggested People', @@ -27,81 +28,74 @@ const suggestionProps: IBasePickerSuggestionsProps = { */ export class PeoplePicker extends React.Component { - public static defaultProps: IPeoplePickerProps = { - context : null, - titleText: "People Picker", - personSelectionLimit: 1, - showtooltip : false, - isRequired : false, - errorMessage : "People picker is mandatory", - groupName: "", - tooltipMessage: "This is a People Picker", - tooltipDirectional: DirectionalHint.leftTopEdge - }; - -/** -* Constructor -*/ -constructor(props: IPeoplePickerProps) { - super(props); - this.state = { - selectedPersons: [], - mostRecentlyUsedPersons: [], - currentSelectedPersons: [], - allPersons: [{ - id: "", - imageUrl: "", - imageInitials: "", - primaryText: "", //Name - secondaryText: "", //Role - tertiaryText: "", //status - optionalText: "" //anything - }], - delayResults: false, - currentPicker: 0, - peoplePartTitle: "", - peoplePartTooltip : "", - isLoading : false, - showmessageerror: false - }; - - if (typeof this.props.selectedItems !== 'undefined' && this.props.selectedItems !== null) { - this.props.selectedItems(this.state.selectedPersons); - }; + constructor(props: IPeoplePickerProps) { + super(props); - appInsights.track('ReactPeoplePicker', { - groupName: !!props.groupName, - name: !!props.groupName, - titleText: !!props.titleText - }); + appInsights.track('ReactPeoplePicker', { + groupName: !!props.groupName, + name: !!props.groupName, + titleText: !!props.titleText + }); - this._onPersonItemsChange = this._onPersonItemsChange.bind(this); + this.state = { + selectedPersons: [], + mostRecentlyUsedPersons: [], + currentSelectedPersons: [], + allPersons: [{ + id: "", + imageUrl: "", + imageInitials: "", + primaryText: "", //Name + secondaryText: "", //Role + tertiaryText: "", //status + optionalText: "" //anything + }], + currentPicker: 0, + peoplePartTitle: "", + peoplePartTooltip : "", + isLoading : false, + showmessageerror: false + }; } + /** + * componentWillMount lifecycle hook + */ public componentWillMount(): void { + // Load the users this._thisLoadUsers(); } + /** + * Generate the user photo link + * + * @param value + */ private generateUserPhotoLink(value : string) : string { - return `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${value}&UA=0&size=HR96x96` - } + return `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${value}&UA=0&size=HR96x96`; + } - private _thisLoadUsers() : void { + /** + * Retrieve the users + */ + private async _thisLoadUsers(): Promise { var stringVal = ""; - if(this.props.groupName != "") - { + if (this.props.groupName) { stringVal = `/_api/web/sitegroups/GetByName('${this.props.groupName}')/users`; - } - else - { + } else { stringVal = "/_api/web/siteusers"; } + // Create the rest API const restApi = `${this.props.context.pageContext.web.absoluteUrl}${stringVal}`; - this.props.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1) - .then(resp => { return resp.json(); }) - .then(items => { - var userValuesArray : any = [{ + + try { + // Call the API endpoint + const items: IUsers = await this.props.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1).then(resp => resp.json()); + + // Check if items were retrieved + if (items && items.value && items.value.length > 0) { + let userValuesArray: any = [{ id: 0, imageUrl: "", imageInitials: "", @@ -110,173 +104,201 @@ constructor(props: IPeoplePickerProps) { tertiaryText: "", //status optionalText: "" //anything }]; - - for(let i = 0; i < items.value.length; i++) - { - if(i == 0) - { - userValuesArray = [{ - id: items.value[i].Id, - imageUrl: this.generateUserPhotoLink(items.value[i].Email), - imageInitials: "", - primaryText: items.value[i].Title, //Name - secondaryText: items.value[i].Email, //Email - tertiaryText: "", //status - optionalText: "" //anything - }] - } - else - { + + // Loop over all the retrieved items + for (let i = 0; i < items.value.length; i++) { + if (i === 0) { + userValuesArray = [{ + id: items.value[i].Id, + imageUrl: this.generateUserPhotoLink(items.value[i].Email), + imageInitials: "", + primaryText: items.value[i].Title, //Name + secondaryText: items.value[i].Email, //Email + tertiaryText: "", //status + optionalText: "" //anything + }]; + } else { userValuesArray.push({ id: items.value[i].Id, - imageUrl: this.generateUserPhotoLink(items.value[i].Email), + imageUrl: this.generateUserPhotoLink(items.value[i].Email), imageInitials: "", primaryText: items.value[i].Title, //Name secondaryText: items.value[i].Email, //Email tertiaryText: "", //status optionalText: "" //anything - }); + }); } } - + let personaList: IPersonaWithMenu[] = []; - userValuesArray.forEach((persona: IPersonaProps) => { + for (const persona of userValuesArray) { let personaWithMenu: IPersonaWithMenu = {}; - assign(personaWithMenu, persona) + assign(personaWithMenu, persona); personaList.push(personaWithMenu); - }); + } + // Update the current state this.setState({ allPersons : userValuesArray, peoplePersonaMenu : personaList, mostRecentlyUsedPersons : personaList.slice(0,5), showmessageerror: this.props.isRequired && this.state.selectedPersons.length === 0 }); - }); + } + } catch (e) { + console.error("Error occured while fetching the users.", e); + } } -@autobind -private _onPersonItemsChange(items: any[]) { + /** + * On persona item changed event + */ + private _onPersonItemsChange = (items: any[]) => { this.setState({ selectedPersons: items, showmessageerror: items.length > 0 ? false : true }); } -@autobind -private _validateInputPeople(input: string) { - if (input.indexOf('@') !== -1) { - return ValidationState.valid; - } else if (input.length > 1) { - return ValidationState.warning; - } else { - return ValidationState.invalid; + /** + * Validates the user input + * + * @param input + */ + private _validateInputPeople = (input: string) => { + if (input.indexOf('@') !== -1) { + return ValidationState.valid; + } else if (input.length > 1) { + return ValidationState.warning; + } else { + return ValidationState.invalid; + } } -} -@autobind -private _returnMostRecentlyUsedPerson(currentPersonas: IPersonaProps[]): IPersonaProps[] | Promise { - let { mostRecentlyUsedPersons } = this.state; - mostRecentlyUsedPersons = this._removeDuplicates(mostRecentlyUsedPersons, currentPersonas); - return this._filterPromise(mostRecentlyUsedPersons); -} + /** + * Returns the most recently used person + * + * @param currentPersonas + */ + private _returnMostRecentlyUsedPerson = (currentPersonas: IPersonaProps[]): IPersonaProps[] => { + let { mostRecentlyUsedPersons } = this.state; + return this._removeDuplicates(mostRecentlyUsedPersons, currentPersonas); + } -@autobind -private _onPersonFilterChanged(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) { - if (filterText) { - let filteredPersonas: IPersonaProps[] = this._filterPersons(filterText); - filteredPersonas = this._removeDuplicates(filteredPersonas, currentPersonas); - filteredPersonas = limitResults ? filteredPersonas.splice(0, limitResults) : filteredPersonas; - return this._filterPromise(filteredPersonas); - } else { - return []; - } -} + /** + * On filter changed event + * + * @param filterText + * @param currentPersonas + * @param limitResults + */ + private _onPersonFilterChanged = (filterText: string, currentPersonas: IPersonaProps[], limitResults?: number): IPersonaProps[] => { + if (filterText) { + let filteredPersonas: IPersonaProps[] = this._filterPersons(filterText); -@autobind -private _filterPersons(filterText: string): IPersonaProps[] { - return this.state.peoplePersonaMenu.filter(item => this._doesTextStartWith(item.primaryText as string, filterText)); -} + filteredPersonas = this._removeDuplicates(filteredPersonas, currentPersonas); + filteredPersonas = limitResults ? filteredPersonas.splice(0, limitResults) : filteredPersonas; + return filteredPersonas; + } else { + return []; + } + } -@autobind -private _removeDuplicates(personas: IPersonaProps[], possibleDupes: IPersonaProps[]) { - return personas.filter(persona => !this._listContainsPersona(persona, possibleDupes)); -} + /** + * Filter persons + * + * @param filterText + */ + private _filterPersons = (filterText: string): IPersonaProps[] => { + return this.state.peoplePersonaMenu.filter(item => this._doesTextStartWith(item.primaryText as string, filterText)); + } -@autobind -private _doesTextStartWith(text: string, filterText: string): boolean { - return text.toLowerCase().indexOf(filterText.toLowerCase()) === 0; -} + /** + * Removes duplicates + * + * @param personas + * @param possibleDupes + */ + private _removeDuplicates = (personas: IPersonaProps[], possibleDupes: IPersonaProps[]): IPersonaProps[] => { + return personas.filter(persona => !this._listContainsPersona(persona, possibleDupes)); + } -@autobind -private _listContainsPersona(persona: IPersonaProps, personas: IPersonaProps[]) { - if (!personas || !personas.length || personas.length === 0) { - return false; + /** + * Checks if text starts with + * + * @param text + * @param filterText + */ + private _doesTextStartWith = (text: string, filterText: string): boolean => { + return text.toLowerCase().indexOf(filterText.toLowerCase()) === 0; } - return personas.filter(item => item.primaryText === persona.primaryText).length > 0; -} -@autobind -private _filterPromise(personasToReturn: IPersonaProps[]): IPersonaProps[] | Promise { - if (this.state.delayResults) { - return this._convertResultsToPromise(personasToReturn); - } else { - return personasToReturn; + /** + * Checks if list contains the person + * + * @param persona + * @param personas + */ + private _listContainsPersona = (persona: IPersonaProps, personas: IPersonaProps[]): boolean => { + if (!personas || !personas.length || personas.length === 0) { + return false; + } + return personas.filter(item => item.primaryText === persona.primaryText).length > 0; } -} -@autobind -private _convertResultsToPromise(results: IPersonaProps[]): Promise { - return new Promise((resolve, reject) => setTimeout(() => resolve(results), 2000)); -} + /** + * Default React component render method + */ + public render(): React.ReactElement { + const peoplepicker = ( +
+ - //#endregion User control function and bindings + peoplePersonaMenu.primaryText} + className={`'ms-PeoplePicker' ${this.props.peoplePickerCntrlclassName ? this.props.peoplePickerCntrlclassName : ''}`} + key={'normal'} + onValidateInput={this._validateInputPeople} + removeButtonAriaLabel={'Remove'} + inputProps={{ + 'aria-label': 'People Picker' + }} + itemLimit={this.props.personSelectionLimit || 1} + onChange={this._onPersonItemsChange} /> +
+ ); -/** - * Default React component render method - */ -public render(): React.ReactElement { - const peoplepicker =
{this.props.titleText} - peoplePersonaMenu.primaryText} - className={ `'ms-PeoplePicker' ${this.props.peoplePickerCntrlclassName ? this.props.peoplePickerCntrlclassName : ''}` } - key={ 'normal' } - onValidateInput={ this._validateInputPeople } - removeButtonAriaLabel={ 'Remove' } - inputProps={ { - 'aria-label': 'People Picker', - //onBlur: (ev: React.FocusEvent) => console.log('onBlur on People Picker called'), - //onFocus: (ev: React.FocusEvent) => console.log('onFocus on People Picker called'), - } } - itemLimit={this.props.personSelectionLimit} - onChange = { this._onPersonItemsChange } - /> -
; - return ( -
- {this.props.showtooltip ? - - {peoplepicker} - : -
- {peoplepicker} -
- } - {(this.props.isRequired && this.state.showmessageerror) ? - - {this.props.errorMessage} - : null - } -
- ); + return ( +
+ { + this.props.showtooltip ? ( + + {peoplepicker} + + ) : ( +
+ {peoplepicker} +
+ ) + } + + { + (this.props.isRequired && this.state.showmessageerror) && ( + + {this.props.errorMessage ? this.props.errorMessage : strings.peoplePickerComponentErrorMessage} + + ) + } +
+ ); } } diff --git a/src/loc/en-us.ts b/src/loc/en-us.ts index f443af6df..84399efbe 100644 --- a/src/loc/en-us.ts +++ b/src/loc/en-us.ts @@ -44,6 +44,10 @@ define([], () => { "TaxonomyPickerExpandTitle": "Expand this Term Set", "TaxonomyPickerMenuTermSet": "Menu for Term Set", "TaxonomyPickerInLabel": "in", - "TaxonomyPickerTermSetLabel": "Term Set" + "TaxonomyPickerTermSetLabel": "Term Set", + + peoplePickerComponentTooltipMessage: "People Picker", + peoplePickerComponentErrorMessage: "People picker is mandatory", + peoplePickerComponentTitleText: "Pick the user(s)", }; }); diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts index 74482a912..6d3bce692 100644 --- a/src/loc/mystrings.d.ts +++ b/src/loc/mystrings.d.ts @@ -1,4 +1,7 @@ declare interface IControlStrings { + peoplePickerComponentTooltipMessage: string; + peoplePickerComponentErrorMessage: string; + peoplePickerComponentTitleText: string; SiteBreadcrumbLabel: string; ListViewGroupEmptyLabel: string; WebPartTitlePlaceholder: string; diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index b95043f9f..73bdd46fc 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -106,7 +106,7 @@ export default class ControlsTest extends React.Component -

Deletes second item

- +

Deletes second item

+ + context={this.props.context} + titleText="People Picker" + // personSelectionLimit={3} + // groupName={"Team Site Owners"} + showtooltip={true} + isRequired={true} + selectedItems={this._getPeoplePickerItems} />
); } From 1ad5309c64fb51121d194d222599b48bb8f68121 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 19 Jun 2018 21:50:34 +0200 Subject: [PATCH 15/15] Fix for #86 --- CHANGELOG.JSON | 5 +++-- CHANGELOG.md | 1 + docs/documentation/docs/about/release-notes.md | 1 + src/controls/listView/ListView.tsx | 17 +++++++++++++++-- .../controlsTest/components/ControlsTest.tsx | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.JSON b/CHANGELOG.JSON index ef97a3d9a..29bf89e61 100644 --- a/CHANGELOG.JSON +++ b/CHANGELOG.JSON @@ -11,10 +11,11 @@ ], "fixes": [ "Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83)", - "`FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84)" + "`FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84)", + "Fixed issue in single selection mode when all group items were selected in the `ListView` when user clicked on the group header [#86](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/86)" ] }, - "contributions": [] + "contributions": ["Asish Padhy", "Alex Terentiev"] }, { "version": "1.4.0", diff --git a/CHANGELOG.md b/CHANGELOG.md index 51c0cfba5..eb732ca97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) - `FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84) +- Fixed issue in single selection mode when all group items were selected in the `ListView` when user clicked on the group header [#86](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/86) ## 1.4.0 diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index 51c0cfba5..eb732ca97 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -14,6 +14,7 @@ - Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) - `FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84) +- Fixed issue in single selection mode when all group items were selected in the `ListView` when user clicked on the group header [#86](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/86) ## 1.4.0 diff --git a/src/controls/listView/ListView.tsx b/src/controls/listView/ListView.tsx index 00df6b637..e6a0cc405 100644 --- a/src/controls/listView/ListView.tsx +++ b/src/controls/listView/ListView.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { DetailsList, DetailsListLayoutMode, Selection, SelectionMode, IGroup } from 'office-ui-fabric-react/lib/DetailsList'; import { IListViewProps, IListViewState, IViewField, IGrouping, GroupOrder } from './IListView'; -import { IColumn } from 'office-ui-fabric-react/lib/components/DetailsList'; +import { IColumn, IGroupRenderProps } from 'office-ui-fabric-react/lib/components/DetailsList'; import { findIndex, has, sortBy, isEqual, cloneDeep } from '@microsoft/sp-lodash-subset'; import { FileTypeIcon, IconType } from '../fileTypeIcon/index'; import * as strings from 'ControlStrings'; @@ -376,6 +376,18 @@ export class ListView extends React.Component { * Default React component render method */ public render(): React.ReactElement { + let groupProps: IGroupRenderProps = {}; + // Check if selection mode is single selection, + // if that is the case, disable the selection on grouping headers + if (this.props.selectionMode === SelectionMode.single) { + groupProps = { + headerProps: { + onToggleSelectGroup: () => null, + onGroupHeaderClick: () => null, + } + }; + } + return (
{ selection={this._selection} layoutMode={DetailsListLayoutMode.justified} compact={this.props.compact} - setKey="ListViewControl" /> + setKey="ListViewControl" + groupProps={groupProps} />
); } diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 73bdd46fc..c06d33ca3 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -296,7 +296,7 @@ export default class ControlsTest extends React.Component

Deletes second item