Features added and fix for issue #1504

This commit is contained in:
Sudharsan K 2020-09-18 23:13:17 +08:00
parent fc3919f9b3
commit 8566f9fe72
15 changed files with 1183 additions and 4586 deletions

View File

@ -28,6 +28,8 @@
![directory](/samples/react-directory/assets/react-directory5.jpg)
![directory](./assets/react-directory-withPaging.png)
## Used SharePoint Framework Version
@ -46,6 +48,8 @@ Property |Type|Required| comments
--------------------|----|--------|----------
Title | Text| no|WebPart Title
searchFirstName | boolean|no| Lastname or Firstname search query
Properties to search | text | no | By default **FirstName,LastName,WorkEmail,Department** are used for search. You can add custom properties separated by comma.
Results per page | number | Number of people result to be displayed per page. Max of **20** is allowed, default of **10** is set.
@ -57,6 +61,7 @@ Solution|Author(s)
--------|---------
Directory Web Part|João Mendes
Directory Web Part| Peter Paul Kirschner ([@petkir_at](https://twitter.com/petkir_at))
Directory Web Part| Sudharsan K ([@sudharsank](https://twitter.com/sudharsank))
## Version history
@ -64,6 +69,7 @@ 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)
2.0.0.0|Sep 18 2020|React hooks, paging, dynamic search props, result alignment using office ui fabric stack.
## Disclaimer

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -26,9 +26,12 @@
"@types/jquery": "^3.3.30",
"@uifabric/fluent-theme": "^0.16.9",
"jquery": "^3.4.1",
"lodash": "^4.17.20",
"office-ui-fabric-react": "6.214.0",
"react": "16.7.0",
"react-dom": "16.7.0"
"react-dom": "16.7.0",
"react-js-pagination": "^3.0.3",
"throttle-debounce": "^2.3.0"
},
"resolutions": {
"@types/react": "16.8.8"

View File

@ -3,5 +3,6 @@ import { PeoplePickerEntity } from '@pnp/sp';
export interface ISPServices {
searchUsers(searchString: string, searchFirstName: boolean);
searchUsersNew(searchString: string, srchQry: string, isInitialSearch: boolean, pageNumber?: number);
}

View File

@ -109,4 +109,35 @@ export class spMockServices implements ISPServices {
}
public async searchUsersNew(searchString: string, srchQry: string, isInitialSearch: boolean, pageNumber?: number) {
let filtervalue = searchString.trim().toLowerCase();
if (searchString.length > 0 && filtervalue.lastIndexOf("*") == searchString.length - 1) {
// remove last '*'
filtervalue = filtervalue.substring(0, searchString.length - 1);
}
if (!filtervalue || filtervalue.length === 0) {
throw new Error("No valid Input.");
}
const searchresult = !!isInitialSearch ?
this.sampleData.filter(p => p.FirstName.toLowerCase().indexOf(filtervalue) === 0) :
this.sampleData.filter(p => p.LastName.toLowerCase().indexOf(filtervalue) === 0);
const timeout = Math.floor(Math.random() * (1000)) + 1;
const resultdata = {
ElapsedTime: timeout,
RowCount: searchresult.length,
TotalRows: searchresult.length,
PrimarySearchResults: searchresult
};
return new Promise((resolve) => {
setTimeout(() => {
resolve(resultdata);
}, timeout);
});
}
}

View File

@ -7,38 +7,86 @@ import { ISPServices } from "./ISPServices";
export class spservices implements ISPServices {
constructor(private context: WebPartContext) {
sp.setup({
spfxContext: this.context
});
}
public async searchUsers(searchString: string, searchFirstName: boolean): Promise<SearchResults> {
const _search = !searchFirstName ? `LastName:${searchString}*` : `FirstName:${searchString}*`;
const searchProperties: string[] = ["FirstName", "LastName", "PreferredName", "WorkEmail", "OfficeNumber", "PictureURL", "WorkPhone", "MobilePhone", "JobTitle", "Department", "Skills", "PastProjects", "BaseOfficeLocation", "SPS-UserType", "GroupId"];
try {
if (!searchString) return undefined;
let users = await sp.searchWithCaching(<SearchQuery>{
Querytext: _search,
RowLimit: 500,
EnableInterleaving: true,
SelectProperties: searchProperties,
SourceId: 'b09a7990-05ea-4af9-81ef-edfab16c4e31',
SortList: [{ "Property": "LastName", "Direction": SortDirection.Ascending }],
});
return users;
} catch (error) {
Promise.reject(error);
constructor(private context: WebPartContext) {
sp.setup({
spfxContext: this.context
});
}
public async searchUsers(searchString: string, searchFirstName: boolean): Promise<SearchResults> {
const _search = !searchFirstName ? `LastName:${searchString}*` : `FirstName:${searchString}*`;
const searchProperties: string[] = ["FirstName", "LastName", "PreferredName", "WorkEmail", "OfficeNumber", "PictureURL", "WorkPhone", "MobilePhone", "JobTitle", "Department", "Skills", "PastProjects", "BaseOfficeLocation", "SPS-UserType", "GroupId"];
try {
if (!searchString) return undefined;
let users = await sp.searchWithCaching(<SearchQuery>{
Querytext: _search,
RowLimit: 500,
EnableInterleaving: true,
SelectProperties: searchProperties,
SourceId: 'b09a7990-05ea-4af9-81ef-edfab16c4e31',
SortList: [{ "Property": "LastName", "Direction": SortDirection.Ascending }],
});
return users;
} catch (error) {
Promise.reject(error);
}
}
public async _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;
});
}
public async searchUsersNew(searchString: string, srchQry: string, isInitialSearch: boolean, pageNumber?: number): Promise<SearchResults> {
//const _search = !isInitialSearch ? srchQry : `FirstName:${searchString}*`;
let qrytext: string = '';
if (isInitialSearch) qrytext = `FirstName:${searchString}* OR LastName:${searchString}*`;
else {
if (srchQry) qrytext = srchQry;
else {
if (searchString) qrytext = searchString;
}
if (qrytext.length <= 0) qrytext = `*`;
}
console.log(qrytext);
const searchProperties: string[] = ["FirstName", "LastName", "PreferredName", "WorkEmail", "OfficeNumber", "PictureURL", "WorkPhone", "MobilePhone", "JobTitle", "Department", "Skills", "PastProjects", "BaseOfficeLocation", "SPS-UserType", "GroupId"];
try {
let users = await sp.search(<SearchQuery>{
Querytext: qrytext,
RowLimit: 500,
EnableInterleaving: true,
SelectProperties: searchProperties,
SourceId: 'b09a7990-05ea-4af9-81ef-edfab16c4e31',
SortList: [{ "Property": "LastName", "Direction": SortDirection.Ascending }],
});
console.log(users);
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: `/_layouts/15/userphoto.aspx?size=M&accountname=${user.WorkEmail}` };
users.PrimarySearchResults[index] = user;
}
}
}
return users;
} catch (error) {
Promise.reject(error);
}
}
}
}

View File

@ -22,7 +22,9 @@
"officeFabricIconFontName": "ProfileSearch",
"properties": {
"title": "Directory",
"searchFirstName": 0
"searchFirstName": 0,
"searchProps": "FirstName,LastName,WorkEmail,Department",
"pageSize": 10
}
}]
}

View File

@ -2,74 +2,108 @@ import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { DisplayMode } from "@microsoft/sp-core-library";
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneToggle,
IPropertyPaneToggleProps
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";
export interface IDirectoryWebPartProps {
title: string;
searchFirstName: boolean;
title: string;
searchFirstName: boolean;
searchProps: string;
pageSize: number;
}
export default class DirectoryWebPart extends BaseClientSideWebPart<
IDirectoryWebPartProps
> {
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;
}
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
IDirectoryWebPartProps
> {
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,
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("title", {
label: strings.TitleFieldLabel
}),
PropertyPaneToggle("searchFirstName", {
checked: false,
label: "Search on First Name ?"
})
]
title: this.properties.title,
context: this.context,
searchFirstName: this.properties.searchFirstName,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.title = value;
},
searchProps: this.properties.searchProps,
pageSize: this.properties.pageSize
}
]
}
]
};
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("title", {
label: strings.TitleFieldLabel
}),
PropertyPaneToggle("searchFirstName", {
checked: false,
label: "Search on First Name ?"
}),
PropertyPaneTextField('searchProps', {
label: strings.SearchPropsLabel,
description: strings.SearchPropsDesc,
value: this.properties.searchProps,
multiline: false,
resizable: false
}),
PropertyPaneSlider('pageSize', {
label: 'Results per page',
showValue: true,
max: 20,
min: 2,
step: 2,
value: this.properties.pageSize
})
]
}
]
}
]
};
}
}

View File

@ -2,7 +2,6 @@
.directory {
.dropDownSortBy {
margin-top: 35px;
margin-bottom: 15px;
}
@ -98,4 +97,18 @@
display: inline-block;
}
}
.alphabets {
padding-left: 10px;
padding-right: 10px;
white-space: normal;
text-align: center;
}
.searchTextBox {
min-width: 180px;
max-width: 300px;
margin-left: auto;
margin-right: auto;
margin-bottom: 25px;
}
}

View File

@ -6,4 +6,6 @@ export interface IDirectoryProps {
context: WebPartContext;
searchFirstName: boolean;
updateProperty: (value: string) => void;
searchProps?: string;
pageSize?: number;
}

View File

@ -4,8 +4,8 @@
display: inline-block;
margin-top: 15px;
margin-right: 15px;
min-width: 300;
min-width: 300px;
max-width: 300px;
vertical-align: top;
}
.documentCard {
@ -25,3 +25,13 @@
margin-top: 10;
width: 100%;
}
.textOverflow {
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}

View File

@ -39,7 +39,6 @@ export class PersonaCard extends React.Component<
LIVE_PERSONA_COMPONENT_ID
);
const livePersonaCard: any = sharedLibrary.LivePersonaCard;
console.log(livePersonaCard);
this.setState({ livePersonaCard: livePersonaCard });
}
}
@ -115,7 +114,7 @@ export class PersonaCard extends React.Component<
''
)}
{this.props.profileProperties.Location ? (
<div>
<div className={styles.textOverflow}>
<Icon iconName="Poi" style={{ fontSize: '12px' }} />
<span style={{ marginLeft: 5, fontSize: '12px' }}>
{' '}

View File

@ -6,6 +6,10 @@ define([], function() {
"PropertyPaneDescription": "Search People from Organization Directory",
"BasicGroupName": "Properties",
"TitleFieldLabel": "Web Part Title",
"DirectoryMessage": "No users found in directory"
"DirectoryMessage": "No users found in directory",
"LoadingText": "Searching for user. Please wait...",
"SearchPropsLabel": "Properties to search",
"SearchPropsDesc": "Enter the properties separated by comma to be used for search",
"PagingLabel": "Results per page"
}
});

View File

@ -6,7 +6,10 @@ declare interface IDirectoryWebPartStrings {
BasicGroupName: string;
TitleFieldLabel: string;
DirectoryMessage: string;
LoadingText: string;
SearchPropsLabel: string;
SearchPropsDesc: string;
PagingLabel: string;
}
declare module 'DirectoryWebPartStrings' {