Merge pull request #3040 from petkir/react-directory

This commit is contained in:
Hugo Bernier 2022-11-07 22:32:44 -05:00 committed by GitHub
commit 3d08873969
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 55 additions and 429 deletions

39
samples/react-directory/README.md Executable file → Normal file
View File

@ -43,10 +43,11 @@ Search People from Organization Directory and show live persona card on hover.
## Applies to
* [SharePoint Online](https://learn.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft Teams](https://products.office.com/en-US/microsoft-teams/group-chat-software)
* [SharePoint Framework](https://learn.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft 365 tenant](https://learn.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/m365devprogram)
## Web Part Properties
@ -74,15 +75,18 @@ Directory Web Part| Abderahman Moujahid
Version|Date|Comments
-------|----|--------
1.0.0|July 29, 2019|Initial release
1.0.1|July 19, 2020|Bugfix and mock-service for workbench (```LivePersonaCard``` not supported in workbench)
1.0.1|July 19, 2020|Bugfix and mock-service for workbench (`LivePersonaCard` not supported in workbench)
2.0.0|Sep 18 2020|React hooks, paging, dynamic search props, result alignment using office ui fabric stack.
3.0.0|Oct 17 2020|Minor fixes and add the additional web part property.
3.0.1|March 4 2021|Bugfix 'Sort People by'
3.0.2|Oct 3 2022|Minor styling fixes and people container position
3.0.3|Oct 4 2022|Fix for LivePersonaCard
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
* Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-directory) then unzip it)
* From your command line, change your current directory to the directory containing this sample (`react-directory`, located under `samples`)
* in the command line run:
- `npm install`
- `gulp build`
- `gulp bundle --ship`
@ -112,4 +116,27 @@ Finally, if you have an idea for improvement, [make a suggestion](https://github
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-directory" />
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
## Help
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-directory%22) to see if anybody else is having the same issues.
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-directory) and see what the community is saying.
If you encounter any issues using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-directory&template=bug-report.yml&sample=react-directory&authors=@joaojmendes%20@petkir_at%20@sudharsank%20@Abderahman88&title=react-directory%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-directory&template=question.yml&sample=react-directory&authors=@joaojmendes%20@petkir_at%20@sudharsank%20@Abderahman88&title=react-directory%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-directory&template=suggestion.yml&sample=react-directory&authors=@joaojmendes%20@petkir_at%20@sudharsank%20@Abderahman88&title=react-directory%20-%20).
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-directory" />

View File

@ -10,7 +10,7 @@
},
"name": "Search Directory",
"id": "5b62bc16-3a71-461d-be2f-16bfcb011e8a",
"version": "3.0.1.0",
"version": "3.0.3.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false

View File

@ -6,12 +6,10 @@ import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneToggle,
IPropertyPaneToggleProps,
PropertyPaneSlider
} from "@microsoft/sp-property-pane";
import * as strings from "DirectoryWebPartStrings";
import Directory from "./components/Directory";
import DirectoryHook from "./components/DirectoryHook";
import { IDirectoryProps } from "./components/IDirectoryProps";
@ -21,6 +19,7 @@ export interface IDirectoryWebPartProps {
searchProps: string;
clearTextSearchProps: string;
pageSize: number;
justifycontent:boolean;
}
export default class DirectoryWebPart extends BaseClientSideWebPart<
@ -28,16 +27,6 @@ export default class DirectoryWebPart extends BaseClientSideWebPart<
> {
public render(): void {
const element: React.ReactElement<IDirectoryProps> = React.createElement(
// Directory,
// {
// title: this.properties.title,
// context: this.context,
// searchFirstName: this.properties.searchFirstName,
// displayMode: this.displayMode,
// updateProperty: (value: string) => {
// this.properties.title = value;
// }
// },
DirectoryHook,
{
title: this.properties.title,
@ -49,7 +38,8 @@ export default class DirectoryWebPart extends BaseClientSideWebPart<
},
searchProps: this.properties.searchProps,
clearTextSearchProps: this.properties.clearTextSearchProps,
pageSize: this.properties.pageSize
pageSize: this.properties.pageSize,
useSpaceBetween:this.properties.justifycontent
}
);
@ -86,6 +76,12 @@ export default class DirectoryWebPart extends BaseClientSideWebPart<
checked: false,
label: "Search on First Name ?"
}),
PropertyPaneToggle("justifycontent", {
checked: false,
label: "Result Layout",
onText:"SpaceBetween",
offText:"Center"
}),
PropertyPaneTextField('searchProps', {
label: strings.SearchPropsLabel,
description: strings.SearchPropsDesc,

View File

@ -103,7 +103,11 @@
padding-right: 10px;
white-space: normal;
text-align: center;
&>div>div{
white-space: normal !important;
}
}
.searchTextBox {
min-width: 180px;
max-width: 300px;
@ -111,4 +115,5 @@
margin-right: auto;
margin-bottom: 25px;
}
}

View File

@ -1,403 +0,0 @@
import * as React from "react";
import styles from "./Directory.module.scss";
import { IDirectoryProps } from "./IDirectoryProps";
import { PersonaCard } from "./PersonaCard/PersonaCard";
import { spservices } from "../../../SPServices/spservices";
import { IDirectoryState } from "./IDirectoryState";
import * as strings from "DirectoryWebPartStrings";
import {
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
SearchBox,
Icon,
Label,
Pivot,
PivotItem,
PivotLinkFormat,
PivotLinkSize,
Dropdown,
IDropdownOption
} from "office-ui-fabric-react";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { ISPServices } from "../../../SPServices/ISPServices";
import { Environment, EnvironmentType } from "@microsoft/sp-core-library";
import { spMockServices } from "../../../SPServices/spMockServices";
const az: string[] = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z"
];
const orderOptions: IDropdownOption[] = [
{ key: "FirstName", text: "First Name" },
{ key: "LastName", text: "Last Name" },
{ key: "Department", text: "Department" },
{ key: "Location", text: "Location" },
{ key: "JobTitle", text: "Job Title" }
];
export default class Directory extends React.Component<
IDirectoryProps,
IDirectoryState
> {
private _services: ISPServices = null;
constructor(props: IDirectoryProps) {
super(props);
this.state = {
users: [],
isLoading: true,
errorMessage: "",
hasError: false,
indexSelectedKey: "A",
searchString: "LastName",
searchText: ""
};
if (Environment.type === EnvironmentType.Local) {
this._services = new spMockServices();
} else {
this._services = new spservices(this.props.context);
}
// Register event handlers
this._searchUsers = this._searchUsers.bind(this);
this._selectedIndex = this._selectedIndex.bind(this);
this._sortPeople = this._sortPeople.bind(this);
this._searchBoxChanged = this._searchBoxChanged.bind(this);
}
/**
*
*
* @memberof Directory
*/
public async componentDidMount() {
await this._searchUsers("A");
}
/**
* Gets image base64
* @param pictureUrl
* @returns
*/
private getImageBase64(pictureUrl: string): Promise<string> {
return new Promise((resolve, reject) => {
let image = new Image();
image.addEventListener("load", () => {
let tempCanvas = document.createElement("canvas");
tempCanvas.width = image.width,
tempCanvas.height = image.height,
tempCanvas.getContext("2d").drawImage(image, 0, 0);
let base64Str;
try {
base64Str = tempCanvas.toDataURL("image/png");
} catch (e) {
return "";
}
resolve(base64Str);
});
image.src = pictureUrl;
});
}
private _searchBoxChanged(newvalue: string): void {
this.setState({ searchText: newvalue }, () => this._searchUsers(newvalue));
}
private async _searchUsers(searchText: string) {
searchText = searchText.trim().length > 0 ? searchText : "A";
this.setState({
isLoading: true,
indexSelectedKey: searchText.substring(0, 1).toLocaleUpperCase(),
searchString: "LastName"
});
try {
const users = await this._services.searchUsers(
searchText,
this.props.searchFirstName
);
if (users && users.PrimarySearchResults.length > 0) {
for (let index = 0; index < users.PrimarySearchResults.length; index++) {
let user: any = users.PrimarySearchResults[index];
if (user.PictureURL) {
user = { ...user, PictureURL: await this.getImageBase64(`/_layouts/15/userphoto.aspx?size=M&accountname=${user.WorkEmail}`) };
users.PrimarySearchResults[index] = user;
}
}
}
this.setState({
users:
users && users.PrimarySearchResults
? users.PrimarySearchResults
: null,
isLoading: false,
errorMessage: "",
hasError: false
});
} catch (error) {
this.setState({ errorMessage: error.message, hasError: true });
}
}
/**
*
*
* @param {IDirectoryProps} prevProps
* @param {IDirectoryState} prevState
* @memberof Directory
*/
public async componentDidUpdate(
prevProps: IDirectoryProps,
prevState: IDirectoryState
) {
if (
this.props.title != prevProps.title ||
this.props.searchFirstName != prevProps.searchFirstName
) {
await this._searchUsers("A");
}
}
/**
*
*
* @private
* @param {string} sortField
* @memberof Directory
*/
private async _sortPeople(sortField: string) {
let _users = this.state.users;
_users = _users.sort((a: any, b: any) => {
switch (sortField) {
// Sorte by FirstName
case "FirstName":
const aFirstName = a.FirstName ? a.FirstName : "";
const bFirstName = b.FirstName ? b.FirstName : "";
if (aFirstName.toUpperCase() < bFirstName.toUpperCase()) {
return -1;
}
if (aFirstName.toUpperCase() > bFirstName.toUpperCase()) {
return 1;
}
return 0;
break;
// Sort by LastName
case "LastName":
const aLastName = a.LastName ? a.LastName : "";
const bLastName = b.LastName ? b.LastName : "";
if (aLastName.toUpperCase() < bLastName.toUpperCase()) {
return -1;
}
if (aLastName.toUpperCase() > bLastName.toUpperCase()) {
return 1;
}
return 0;
break;
// Sort by Location
case "Location":
const aBaseOfficeLocation = a.BaseOfficeLocation
? a.BaseOfficeLocation
: "";
const bBaseOfficeLocation = b.BaseOfficeLocation
? b.BaseOfficeLocation
: "";
if (
aBaseOfficeLocation.toUpperCase() <
bBaseOfficeLocation.toUpperCase()
) {
return -1;
}
if (
aBaseOfficeLocation.toUpperCase() >
bBaseOfficeLocation.toUpperCase()
) {
return 1;
}
return 0;
break;
// Sort by JobTitle
case "JobTitle":
const aJobTitle = a.JobTitle ? a.JobTitle : "";
const bJobTitle = b.JobTitle ? b.JobTitle : "";
if (aJobTitle.toUpperCase() < bJobTitle.toUpperCase()) {
return -1;
}
if (aJobTitle.toUpperCase() > bJobTitle.toUpperCase()) {
return 1;
}
return 0;
break;
// Sort by Department
case "Department":
const aDepartment = a.Department ? a.Department : "";
const bDepartment = b.Department ? b.Department : "";
if (aDepartment.toUpperCase() < bDepartment.toUpperCase()) {
return -1;
}
if (aDepartment.toUpperCase() > bDepartment.toUpperCase()) {
return 1;
}
return 0;
break;
default:
break;
}
});
this.setState({ users: _users, searchString: sortField });
}
/**
*
*
* @private
* @param {PivotItem} [item]
* @param {React.MouseEvent<HTMLElement>} [ev]
* @memberof Directory
*/
private _selectedIndex(item?: PivotItem, ev?: React.MouseEvent<HTMLElement>) {
this.setState({ searchText: "" }, () => this._searchUsers(item.props.itemKey));
}
/**
*
*
* @returns {React.ReactElement<IDirectoryProps>}
* @memberof Directory
*/
public render(): React.ReactElement<IDirectoryProps> {
const color = this.props.context.microsoftTeams ? "white" : "";
const diretoryGrid =
this.state.users && this.state.users.length > 0
? this.state.users.map((user: any) => {
return (
<PersonaCard
context={this.props.context}
profileProperties={{
DisplayName: user.PreferredName,
Title: user.JobTitle,
PictureUrl: user.PictureURL,
Email: user.WorkEmail,
Department: user.Department,
WorkPhone: user.WorkPhone,
Location: user.OfficeNumber
? user.OfficeNumber
: user.BaseOfficeLocation
}}
/>
);
})
: [];
return (
<div className={styles.directory}>
<WebPartTitle
displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty}
/>
<div className={styles.searchBox}>
<SearchBox
placeholder={strings.SearchPlaceHolder}
styles={{
root: {
minWidth: 180,
maxWidth: 300,
marginLeft: "auto",
marginRight: "auto",
marginBottom: 25
}
}}
onSearch={this._searchUsers}
onClear={() => {
this._searchUsers("A");
}}
value={this.state.searchText}
onChange={this._searchBoxChanged}
/>
<div>
<Pivot
styles={{
root: {
paddingLeft: 10,
paddingRight: 10,
whiteSpace: "normal",
textAlign: "center"
}
}}
linkFormat={PivotLinkFormat.tabs}
selectedKey={this.state.indexSelectedKey}
onLinkClick={this._selectedIndex}
linkSize={PivotLinkSize.normal}
>
{az.map((index: string) => {
return (
<PivotItem headerText={index} itemKey={index} key={index} />
);
})}
</Pivot>
</div>
</div>
{!this.state.users || this.state.users.length == 0 ? (
<div className={styles.noUsers}>
<Icon
iconName={"ProfileSearch"}
style={{ fontSize: "54px", color: color }}
/>
<Label>
<span style={{ marginLeft: 5, fontSize: "26px", color: color }}>
{strings.DirectoryMessage}
</span>
</Label>
</div>
) : this.state.isLoading ? (
<Spinner size={SpinnerSize.large} label={"searching ..."} />
) : this.state.hasError ? (
<MessageBar messageBarType={MessageBarType.error}>
{this.state.errorMessage}
</MessageBar>
) : (
<div className={styles.dropDownSortBy}>
<Dropdown
placeholder={strings.DropDownPlaceHolderMessage}
label={strings.DropDownPlaceLabelMessage}
options={orderOptions}
selectedKey={this.state.searchString}
onChange={(ev: any, value: IDropdownOption) => {
this._sortPeople(value.key.toString());
}}
styles={{ dropdown: { width: 200 } }}
/>
<div>{diretoryGrid}</div>
</div>
)}
</div>
);
}
}

View File

@ -9,7 +9,7 @@ import {
Spinner, SpinnerSize, MessageBar, MessageBarType, SearchBox, Icon, Label,
Pivot, PivotItem, PivotLinkFormat, PivotLinkSize, Dropdown, IDropdownOption
} from "office-ui-fabric-react";
import { Stack, IStackStyles, IStackTokens } from 'office-ui-fabric-react/lib/Stack';
import { Stack, IStackTokens } from 'office-ui-fabric-react/lib/Stack';
import { debounce } from "throttle-debounce";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { ISPServices } from "../../../SPServices/ISPServices";
@ -356,8 +356,8 @@ const DirectoryHook: React.FC<IDirectoryProps> = (props) => {
/>
</Stack>
</div>
<Stack horizontal horizontalAlign="center" wrap tokens={wrapStackTokens}>
<div>{diretoryGrid}</div>
<Stack horizontal horizontalAlign={props.useSpaceBetween?"space-between":"center"} wrap tokens={wrapStackTokens}>
{diretoryGrid}
</Stack>
<div style={{ width: '100%', display: 'inline-block' }}>
<Paging

View File

@ -9,4 +9,5 @@ export interface IDirectoryProps {
searchProps?: string;
clearTextSearchProps?: string;
pageSize?: number;
useSpaceBetween?:boolean;
}

View File

@ -67,7 +67,7 @@ export class PersonaCard extends React.Component<
this.state.livePersonaCard,
{
serviceScope: this.props.context.serviceScope,
upn: this.props.profileProperties.Email,
legacyUpn: this.props.profileProperties.Email,
onCardOpen: () => {
console.log('LivePersonaCard Open');
},