Skip to content

Commit

Permalink
Merge pull request #3550 from jblueml/jblueml-pnp-persona-presence-info
Browse files Browse the repository at this point in the history
Add "Show presence"-option to People-Layout
  • Loading branch information
wobba authored Feb 22, 2024
2 parents c74dbbd + c46c375 commit 8724173
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 13 deletions.
25 changes: 15 additions & 10 deletions docs/extensibility/web_components_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

Here are the list of all **reusable** web components you can use to customize your templates.

- [<pnp-iconfile>](#ltpnp-iconfilegt)
- [<pnp-documentcard>](#ltpnp-documentcardgt)
- [<pnp-filepreview>](#ltpnp-filepreviewgt)
- [<pnp-icon>](#ltpnp-icongt)
- [<pnp-panel>](#ltpnp-panelgt)
- [<pnp-collapsible>](#ltpnp-collapsiblegt)
- [<pnp-persona>](#ltpnp-personagt)
- [<pnp-img>](#ltpnp-imggt)
- [<pnp-breadcrumb>](#ltpnp-breadcrumbgt)
- [Builtin web components](#builtin-web-components)
- [`<pnp-iconfile>`](#pnp-iconfile)
- [`<pnp-documentcard>`](#pnp-documentcard)
- [`<pnp-filepreview>`](#pnp-filepreview)
- [`<pnp-icon>`](#pnp-icon)
- [`<pnp-panel>`](#pnp-panel)
- [`<pnp-collapsible>`](#pnp-collapsible)
- [`<pnp-persona>`](#pnp-persona)
- [`<pnp-img>`](#pnp-img)
- [`<pnp-breadcrumb>`](#pnp-breadcrumb)

> All other web components you will see in builtin layout templates are considered **internal** and are not supported for custom use.
Expand Down Expand Up @@ -191,7 +192,9 @@ Here are the list of all **reusable** web components you can use to customize yo
data-tertiary-text=""
data-optional-text="514 928 0000"
data-persona-size=""
data-native-lpc=true >
data-native-lpc=true
data-show-presence=true
data-user-object-id="[GUID]">
</pnp-persona>
```

Expand All @@ -204,6 +207,8 @@ Here are the list of all **reusable** web components you can use to customize yo
|**data-optional-text**|The optional text to display.
|**data-persona-size**|The size of the persona **item** to display (no only the picture). Valid values are <ul><li>tiny = 0</li><li>extraExtraSmall = 1</li><li>extraSmall = 2</li><li>small = 3</li><li>regular = 4</li><li>large = 5</li><li>extraLarge = 6</li></ul>
|**data-native-lpc**|Enable SharePoint native Live Persona Card on hover.
|**data-show-presence**|Show the user's presence-information.
|**data-user-object-id**|The person's Entra ID Object-ID (a GUID normally provided by the default-slot "PersonQuery" which is mapped to managed property "AADObjectID")

## `<pnp-img>`
- **Description**: Display an image with support for fallback behavior.
Expand Down
1 change: 1 addition & 0 deletions docs/usage/search-results/layouts/people.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The 'people' layout display a list of persons with additional information. Typic
| **Manage people fields** | Allows you to define you own values for people placeholder fields. <br><p align="center">[!["Manage people fields"](../../../assets/webparts/search-results/layouts/manage_people_fields.png)](../../../assets/webparts/search-results/layouts/manage_people_fields.png)</p>As a field value, you can choose either a field property value (from the list or as free text) and without any transformation or use an Handlebars expression by clicking on the checkbox next to it. In this case, all helpers from the main template are available. Also, if the field doesn't have the **'Allow HTML'** indication flag enabled, it means the value will be always interpreted as text, regardless if you set an HTML value. Otherwise, your value will be interpreted as HTML for those fields (ex: '_Primary text_' placeholder field). For HTML fields you can use the special variable `@root.theme` to use theme colors (ex: `@root.theme.palette.themePrimary`) or `@root.slots.<SlotName>` to access slot value. If you don't set a value for those fields (i.e an empty value), they won't appear in the UI.</br>
| **Show persona card on hover (LPC)** | If enabled, show a person card on hover for the curren item using the native SharePoint implementation.
| **Show persona card on hover** | If enabled, show a person card on hover for the current item. <p align="center">[!["Persona card hover"](../../../assets/webparts/search-results/layouts/persona_card_hover.png)](../../../assets/webparts/search-results/layouts/persona_card_hover.png)</p> This feature uses Microsoft Graph and [Microsoft Graph Toolkit](https://docs.microsoft.com/en-us/graph/toolkit/components/person) to display information about the user and needs the following API permissions in your tenant to work: <ul><li>User.Read</li><li>People.Read</li><li>Contacts.Read</li><li>User.Read.All</li></ul>**If these permissions are not set, the card won't appear**. You can use [PnP Office 365 CLI](https://pnp.github.io/office365-cli/cmd/spo/serviceprincipal/serviceprincipal-grant-add/) to add correct permissions for this feature:</br></br>`$m365 spo serviceprincipal grant add --resource '<aad_app_display_name>' --scope 'user_impersonation'`. Refer to the section below about [persona hover card customization](#persona-hover-card).
| **Show presence** | <p>If enabled, the person's presence-information will be displayed in the bottom right corner of the user's profile picture.</p>This feature uses Microsoft Graph and needs the API permission 'Presence.Read.All' in your tenant to work.
| **Component size** | The size of the person item (not only the picture). The more the size is and the more information will be displayed for each item and vice versa.

#### Persona hover card
Expand Down
117 changes: 115 additions & 2 deletions search-parts/src/components/PersonaComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Persona, IPersonaProps, IPersonaSharedProps, getInitials, Icon, ITheme } from '@fluentui/react';
import { Persona, IPersonaProps, IPersonaSharedProps, getInitials, Icon, ITheme, PersonaPresence } from '@fluentui/react';
import { TemplateService } from "../services/templateService/TemplateService";
import * as ReactDOM from 'react-dom';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
Expand All @@ -10,8 +10,11 @@ import { UrlHelper } from '../helpers/UrlHelper';
import { isEmpty } from '@microsoft/sp-lodash-subset';
import { DomPurifyHelper } from '../helpers/DomPurifyHelper';
import { IComponentFieldsConfiguration } from '../models/common/IComponentFieldsConfiguration';
import { ServiceScope, ServiceKey } from '@microsoft/sp-core-library';
import { ServiceScope, ServiceKey, Log } from '@microsoft/sp-core-library';
import { LivePersona } from "@pnp/spfx-controls-react/lib/LivePersona";
import { MSGraphClientFactory } from '@microsoft/sp-http';

const LogSource = "PersonaComponent";

export interface IPersonaComponentProps {

Expand Down Expand Up @@ -81,9 +84,26 @@ export interface IPersonaComponentProps {
* Enable native LPC from SharePoint
*/
nativeLpc?: boolean;

/**
* Show presence information?
*/
showPresence?: boolean;

/**
* The person's Entra ID Object-ID (usually passed via default-slot "PersonQuery")
*/
userObjectId?: string;
}

export interface IPresenceInfo {
Presence: PersonaPresence;
Activity: string;
}

export interface IPersonaComponentState {
PresenceProcessed: boolean;
PresenceInfo: IPresenceInfo;
}

export class PersonaComponent extends React.Component<IPersonaComponentProps, IPersonaComponentState> {
Expand All @@ -101,6 +121,35 @@ export class PersonaComponent extends React.Component<IPersonaComponentProps, IP

this._domPurify.addHook('uponSanitizeElement', DomPurifyHelper.allowCustomComponentsHook);
this._domPurify.addHook('uponSanitizeAttribute', DomPurifyHelper.allowCustomAttributesHook);

this.state = {
PresenceProcessed: false,
PresenceInfo: undefined
}
}

public async componentDidMount(): Promise<void> {

if (this.props.showPresence && this.props.userObjectId && !this.state.PresenceProcessed) {

// get presence-information via MS Graph asynchronously
this.getUserPresenceInfo(this.props.userObjectId)
.then((presenceInfo) => {
this.setState({
PresenceProcessed: true,
PresenceInfo: presenceInfo
});
})
.catch((error) => {
// in case of an error, simply set state "PresenceProcessed" to "true" and leave "PresenceInfo" = "undefined"
Log.error(LogSource, error, this.props.serviceScope);
this.setState({ PresenceProcessed: true });
});
}
else {
// if not showing presence-information, simply set state "PresenceProcessed" to "true" and leave "PresenceInfo" = "undefined"
this.setState({ PresenceProcessed: true });
}
}

public render() {
Expand Down Expand Up @@ -145,6 +194,12 @@ export class PersonaComponent extends React.Component<IPersonaComponentProps, IP
}
};

// if PresenceInfo is present, set appropriate props in IPersonaProps
if (this.state.PresenceInfo) {
persona.presence = this.state.PresenceInfo.Presence;
persona.presenceTitle = this.state.PresenceInfo.Activity;
}

if (this.props.nativeLpc) {
return <LivePersona upn={processedProps.tertiaryText}
template={
Expand All @@ -158,6 +213,64 @@ export class PersonaComponent extends React.Component<IPersonaComponentProps, IP

return <Persona {...persona} size={parseInt(this.props.personaSize)}></Persona>;
}

/**
* Performs a MS Graph-call to retrieve presence-information of an Entra ID-user
* @param entraIdUserObjectId Entra ID ObjectId of the user
* @returns Object of type "IPresenceInfo" containing Presence- and Activity-information
*/
private getUserPresenceInfo(entraIdUserObjectId: string): Promise<IPresenceInfo> {

return new Promise<IPresenceInfo>((resolve, reject) => {

const msGraphClientFactory = this.props.serviceScope.consume<MSGraphClientFactory>(MSGraphClientFactory.serviceKey);
msGraphClientFactory.getClient("3")
.then((client) => {
client.api(`/users/${entraIdUserObjectId}/presence`)
.get((error, response: any, rawResponse?: any) => {

if (error === null && response) {
resolve({
Presence: this.getPersonaPresenceFromAvailability(response.availability),
Activity: response.activity
});
}
else if (error) { reject(error); }
})
})
.catch((error) => { reject(error); })
});
}

/**
* Returns the Enum-value corresponding to MS Graph's "availability"-string
* @param availability String-value "availability" from MS Graph
* @returns PersonaPresence Enum-value
*/
private getPersonaPresenceFromAvailability(availability: string): PersonaPresence {
switch (availability) {
case 'Busy':
case 'BusyIdle':
return PersonaPresence.busy;

case 'Available':
case 'AvailableIdle':
return PersonaPresence.online;

case 'Away':
case 'BeRightBack':
return PersonaPresence.away;

case 'Offline':
return PersonaPresence.offline;

case 'DoNotDisturb':
return PersonaPresence.dnd;

default:
return PersonaPresence.none;
}
}
}

export class PersonaWebComponent extends BaseWebComponent {
Expand Down
14 changes: 14 additions & 0 deletions search-parts/src/layouts/results/people/PeopleLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export interface IPeopleLayoutProperties {
* Flag indicating if the persona card should be displayed on hover using native LPC
*/
showPersonaCardNative: boolean;

/**
* Flag indicating whether to show presence-information or not
*/
showPersonaPresenceInfo: boolean;
}

export class PeopleLayout extends BaseLayout<IPeopleLayoutProperties> {
Expand Down Expand Up @@ -170,6 +175,15 @@ export class PeopleLayout extends BaseLayout<IPeopleLayoutProperties> {
offText: strings.General.OffTextLabel,
checked: this.properties.showPersonaCard,
}),
this._propertyFieldToogleWithCallout('layoutProperties.showPersonaPresenceInfo', {
label: strings.Layouts.People.ShowPersonaPresenceInfo,
calloutTrigger: this._propertyFieldCalloutTriggers.Hover,
key: 'layoutProperties.showPersonaPresenceInfo',
calloutContent: React.createElement('p', { style: { maxWidth: 250, wordBreak: 'break-word' } }, strings.Layouts.People.ShowPersonaPresenceInfoCalloutMsg),
onText: strings.General.OnTextLabel,
offText: strings.General.OffTextLabel,
checked: this.properties.showPersonaPresenceInfo
}),
PropertyPaneChoiceGroup('layoutProperties.personaSize', {
label: strings.Layouts.People.PersonaSizeOptionsLabel,
options: [
Expand Down
6 changes: 5 additions & 1 deletion search-parts/src/layouts/results/people/people.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@
data-persona-size="{{@root.properties.layoutProperties.personaSize}}"
data-theme-variant="{{JSONstringify @root.theme}}"
data-instance-id="{{@root.instanceId}}"
data-context="{{JSONstringify (truncateContext @root)}}">
data-context="{{JSONstringify (truncateContext @root)}}"
data-show-presence={{@root.properties.layoutProperties.showPersonaPresenceInfo}}
data-user-object-id="{{slot item @root.slots.PersonQuery}}">
</pnp-persona>
</template>
<template data-type="person-card">
Expand All @@ -76,6 +78,8 @@
data-instance-id="{{@root.instanceId}}"
data-theme-variant="{{JSONstringify @root.theme}}"
data-native-lpc={{@root.properties.layoutProperties.showPersonaCardNative}}
data-show-presence={{@root.properties.layoutProperties.showPersonaPresenceInfo}}
data-user-object-id="{{slot item @root.slots.PersonQuery}}"
></pnp-persona>
{{/and}}

Expand Down
2 changes: 2 additions & 0 deletions search-parts/src/loc/commonStrings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ declare interface ICommonStrings {
ShowPersonaCardOnHoverNative: string;
ShowPersonaCardOnHoverCalloutMsg: string;
ShowPersonaCardOnHoverCalloutMsgNative: string;
ShowPersonaPresenceInfo: string;
ShowPersonaPresenceInfoCalloutMsg: string;
Fields: {
ImageUrl: string;
PrimaryText: string;
Expand Down
2 changes: 2 additions & 0 deletions search-parts/src/loc/da-dk.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ define([], function() {
ShowPersonaCardOnHover: "Vis persona-card ved at hover",
ShowPersonaCardOnHoverNative: "Vis persona-card ved at hover (LPC)",
ShowPersonaCardOnHoverCalloutMsg: "Denne feature bruger Microsoft Graph til at vise information om brugeren og skal bruge de følgende API-tilladelser i din tenant, for at det virker: ['User.Read','People.Read','Contacts.Read','User.Read.All'].",
ShowPersonaPresenceInfo: "Vis tilstedeværelse",
ShowPersonaPresenceInfoCalloutMsg: "Denne funktion kræver følgende API-tilladelser i din tenant for at fungere: ['Presence.Read.All']",
Fields: {
ImageUrl: "Billede-URL",
PrimaryText: "Primær tekst",
Expand Down
2 changes: 2 additions & 0 deletions search-parts/src/loc/de-de.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ define([], function () {
ShowPersonaCardOnHoverCalloutMsg: "Diese Funktion verwendet Microsoft Graph, um Informationen über den Benutzer anzuzeigen. Sie benötigt die folgenden API-Berechtigungen in Ihrem Mandanten, um zu funktionieren: ['User.Read','People.Read','Contacts.Read','User.Read.All'].",
ShowPersonaCardOnHoverNative: "Persona-Karte bei Hover anzeigen (LPC)",
ShowPersonaCardOnHoverCalloutMsgNative: "Diese Funktion verwendet die Standard-Komponente von SharePoint, um die LivePersona-Karte anzuzeigen. Beachten Sie den Disclaimer unter https://pnp.github.io/sp-dev-fx-controls-react/controls/LivePersona/.",
ShowPersonaPresenceInfo: "Präsenz anzeigen",
ShowPersonaPresenceInfoCalloutMsg: "Diese Funktion benötigt die folgende API-Berechtigungen in Ihrem Mandanten, um zu funktionieren: ['Presence.Read.All']",
Fields: {
ImageUrl: "Bild URL",
PrimaryText: "Primärer Text",
Expand Down
2 changes: 2 additions & 0 deletions search-parts/src/loc/en-us.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ define([], function() {
ShowPersonaCardOnHoverCalloutMsg: "This feature uses Microsoft Graph to display information about the user and needs the following API permissions in your tenant to work: ['User.Read','People.Read','Contacts.Read','User.Read.All'].",
ShowPersonaCardOnHoverNative: "Show persona card on hover (LPC)",
ShowPersonaCardOnHoverCalloutMsgNative: "This feature uses the native SharePoint implementation to show the live persona card (LPC). See https://pnp.github.io/sp-dev-fx-controls-react/controls/LivePersona/ for considerations.",
ShowPersonaPresenceInfo: "Show presence",
ShowPersonaPresenceInfoCalloutMsg: "This feature needs the following API permissions in your tenant to work: ['Presence.Read.All']",
Fields: {
ImageUrl: "Image URL",
PrimaryText: "Primary text",
Expand Down
2 changes: 2 additions & 0 deletions search-parts/src/loc/es-es.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ define([], function() {
ShowPersonaCardOnHover: "Mostrar tarjeta de persona al pasar el ratón por encima",
ShowPersonaCardOnHoverNative:"Mostrar tarjeta de persona al pasar el ratón por encima (LPC)",
ShowPersonaCardOnHoverCalloutMsg: "Esta función utiliza Microsoft Graph para mostrar información sobre el usuario y necesita los siguientes permisos de la API en su inquilino para funcionar: ['User.Read','People.Read','Contacts.Read','User.Read.All'].",
ShowPersonaPresenceInfo: "Mostrar presencia",
ShowPersonaPresenceInfoCalloutMsg: "Esta función necesita los siguientes permisos de API en su tenant para funcionar: ['Presence.Read.All']",
Fields: {
ImageUrl: "URL de la imagen",
PrimaryText: "Texto principal",
Expand Down
2 changes: 2 additions & 0 deletions search-parts/src/loc/fi-fi.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ define([], function() {
ShowPersonaCardOnHover: "Näytä henkilökortti hoverilla",
ShowPersonaCardOnHoverNative: "Näytä henkilökortti hoverilla (LPC)",
ShowPersonaCardOnHoverCalloutMsg: "Tämä ominaisuus käyttää Microsoft Graphia käyttäjäprofiilin tietojen näyttämiseen, ja tarvitsee seuraavat API oikeudet tenantissa toimiakseen: ['User.Read','People.Read','Contacts.Read','User.Read.All'].",
ShowPersonaPresenceInfo: "Näytä läsnäolo",
ShowPersonaPresenceInfoCalloutMsg: "Tämä ominaisuus vaatii seuraavat API-oikeudet vuokralaisessasi toimiakseen: ['Presence.Read.All']",
Fields: {
ImageUrl: "Kuvan URL",
PrimaryText: "Ensimmäisen rivin teksti",
Expand Down
Loading

0 comments on commit 8724173

Please sign in to comment.