From c46c3755f47772e99c59c9125a65172dfcf03ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Bl=C3=BCml?= Date: Wed, 21 Feb 2024 11:12:35 +0100 Subject: [PATCH] Add "Show presence"-option to People-Layout --- docs/extensibility/web_components_list.md | 25 ++-- docs/usage/search-results/layouts/people.md | 1 + .../src/components/PersonaComponent.tsx | 117 +++++++++++++++++- .../layouts/results/people/PeopleLayout.ts | 14 +++ .../src/layouts/results/people/people.html | 6 +- search-parts/src/loc/commonStrings.d.ts | 2 + search-parts/src/loc/da-dk.js | 2 + search-parts/src/loc/de-de.js | 2 + search-parts/src/loc/en-us.js | 2 + search-parts/src/loc/es-es.js | 2 + search-parts/src/loc/fi-fi.js | 2 + search-parts/src/loc/fr-fr.js | 2 + search-parts/src/loc/nb-no.js | 2 + search-parts/src/loc/nl-nl.js | 2 + search-parts/src/loc/pl-pl.js | 2 + search-parts/src/loc/pt-br.js | 2 + search-parts/src/loc/sv-SE.js | 2 + 17 files changed, 174 insertions(+), 13 deletions(-) diff --git a/docs/extensibility/web_components_list.md b/docs/extensibility/web_components_list.md index 9b502d81..7b1801f9 100644 --- a/docs/extensibility/web_components_list.md +++ b/docs/extensibility/web_components_list.md @@ -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-documentcard) + - [``](#pnp-filepreview) + - [``](#pnp-icon) + - [``](#pnp-panel) + - [``](#pnp-collapsible) + - [``](#pnp-persona) + - [``](#pnp-img) + - [``](#pnp-breadcrumb) > All other web components you will see in builtin layout templates are considered **internal** and are not supported for custom use. @@ -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]"> ``` @@ -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
  • tiny = 0
  • extraExtraSmall = 1
  • extraSmall = 2
  • small = 3
  • regular = 4
  • large = 5
  • extraLarge = 6
|**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") ## `` - **Description**: Display an image with support for fallback behavior. diff --git a/docs/usage/search-results/layouts/people.md b/docs/usage/search-results/layouts/people.md index 7a351e3b..deb2f9e1 100644 --- a/docs/usage/search-results/layouts/people.md +++ b/docs/usage/search-results/layouts/people.md @@ -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.

[!["Manage people fields"](../../../assets/webparts/search-results/layouts/manage_people_fields.png)](../../../assets/webparts/search-results/layouts/manage_people_fields.png)

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.` 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.
| **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.

[!["Persona card hover"](../../../assets/webparts/search-results/layouts/persona_card_hover.png)](../../../assets/webparts/search-results/layouts/persona_card_hover.png)

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:
  • User.Read
  • People.Read
  • Contacts.Read
  • User.Read.All
**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:

`$m365 spo serviceprincipal grant add --resource '' --scope 'user_impersonation'`. Refer to the section below about [persona hover card customization](#persona-hover-card). +| **Show presence** |

If enabled, the person's presence-information will be displayed in the bottom right corner of the user's profile picture.

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 diff --git a/search-parts/src/components/PersonaComponent.tsx b/search-parts/src/components/PersonaComponent.tsx index 68e57421..ab2d36eb 100644 --- a/search-parts/src/components/PersonaComponent.tsx +++ b/search-parts/src/components/PersonaComponent.tsx @@ -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'; @@ -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 { @@ -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 { @@ -101,6 +121,35 @@ export class PersonaComponent extends React.Component { + + 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() { @@ -145,6 +194,12 @@ export class PersonaComponent extends React.Component; } + + /** + * 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 { + + return new Promise((resolve, reject) => { + + const msGraphClientFactory = this.props.serviceScope.consume(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 { diff --git a/search-parts/src/layouts/results/people/PeopleLayout.ts b/search-parts/src/layouts/results/people/PeopleLayout.ts index dad28340..9dafa425 100644 --- a/search-parts/src/layouts/results/people/PeopleLayout.ts +++ b/search-parts/src/layouts/results/people/PeopleLayout.ts @@ -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 { @@ -170,6 +175,15 @@ export class PeopleLayout extends BaseLayout { 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: [ diff --git a/search-parts/src/layouts/results/people/people.html b/search-parts/src/layouts/results/people/people.html index 44d975e7..941a5bc0 100644 --- a/search-parts/src/layouts/results/people/people.html +++ b/search-parts/src/layouts/results/people/people.html @@ -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}}">