From 503fd0f30ce6dbe3cac82f436605bb63fa9b5ca1 Mon Sep 17 00:00:00 2001 From: Scott Durow Date: Tue, 1 Nov 2022 01:40:58 -0700 Subject: [PATCH] (People Picker) Deterministic Suggestions Resolution (#51) * fixes issue with suggestions not resolving correctly fixes other minor typos and consistency issues * fix casing on peoplepicker file --- .../PeoplePicker/ControlManifest.Input.xml | 2 +- .../PeoplePicker/__mocks__/mock-parameters.ts | 2 +- .../peoplepicker-lifecycle.test.tsx.snap | 4 +- .../__tests__/peoplepicker-component.test.tsx | 10 +- .../components/Component.types.ts | 4 +- .../PeoplePicker/components/DatasetMapping.ts | 2 +- .../PeoplePicker/components/Peoplepicker.tsx | 28 +-- .../PeoplePicker/css/PeoplePicker.css | 6 +- PeoplePicker/PeoplePicker/index.ts | 162 +++++++++++------- .../strings/PeoplePicker.1033.resx | 4 +- PeoplePicker/package-lock.json | 24 +++ PeoplePicker/package.json | 2 + PeoplePicker/readme.md | 4 +- 13 files changed, 163 insertions(+), 91 deletions(-) diff --git a/PeoplePicker/PeoplePicker/ControlManifest.Input.xml b/PeoplePicker/PeoplePicker/ControlManifest.Input.xml index 6c177415..b0ab3390 100644 --- a/PeoplePicker/PeoplePicker/ControlManifest.Input.xml +++ b/PeoplePicker/PeoplePicker/ControlManifest.Input.xml @@ -7,7 +7,7 @@ - + diff --git a/PeoplePicker/PeoplePicker/__mocks__/mock-parameters.ts b/PeoplePicker/PeoplePicker/__mocks__/mock-parameters.ts index 0b040e7b..c8a71bdf 100644 --- a/PeoplePicker/PeoplePicker/__mocks__/mock-parameters.ts +++ b/PeoplePicker/PeoplePicker/__mocks__/mock-parameters.ts @@ -17,7 +17,7 @@ export function getMockParameters(): IInputs { MinimumSearchTermLength: new MockWholeNumberProperty(), SearchTermToShortMessage: new MockStringProperty(), SuggestionsHeaderText: new MockStringProperty(), - NoResultFoundMesage: new MockStringProperty(), + NoResultFoundMessage: new MockStringProperty(), HintText: new MockStringProperty(), MaxPeople: new MockWholeNumberProperty(), }; diff --git a/PeoplePicker/PeoplePicker/__tests__/__snapshots__/peoplepicker-lifecycle.test.tsx.snap b/PeoplePicker/PeoplePicker/__tests__/__snapshots__/peoplepicker-lifecycle.test.tsx.snap index 167fb4a1..89e4b696 100644 --- a/PeoplePicker/PeoplePicker/__tests__/__snapshots__/peoplepicker-lifecycle.test.tsx.snap +++ b/PeoplePicker/PeoplePicker/__tests__/__snapshots__/peoplepicker-lifecycle.test.tsx.snap @@ -3,7 +3,7 @@ exports[`PeoplePicker renders 1`] = `
{ @@ -29,7 +29,7 @@ describe('PeoplePickerComponent', () => { it('test getDataSetfromPersona function', () => { const personaValue = [{ key: '1', text: 'John Doe' }] as IPersonaProps[]; - const personaOutput = getDataSetfromPersona(personaValue); + const personaOutput = getDataSetFromPersona(personaValue); expect(personaOutput[0].PersonaKey).toBe(personaOutput[0].PersonaKey); }); @@ -48,7 +48,7 @@ describe('PeoplePickerComponent', () => { width: 300, height: 32, people: [{ key: '1', text: 'John Doe' }] as IPersonaProps[], - peoplepickerType: 'normal people picker', + peoplePickerType: 'normal people picker', defaultSelected: [] as IPersonaProps[], delayResults: false, isPickerDisabled: false, @@ -66,7 +66,7 @@ describe('PeoplePickerComponent', () => { keepTypingMessage: 'Continue typing...', suggestionsHeaderText: 'Suggested People', filterSuggestions: filterSuggestions, - noresultfoundText: 'no result found', + noResultFoundText: 'no result found', onBlur: onBlur, onFocus: onFocus, hintText: 'Search', diff --git a/PeoplePicker/PeoplePicker/components/Component.types.ts b/PeoplePicker/PeoplePicker/components/Component.types.ts index c1c135e2..55b48544 100644 --- a/PeoplePicker/PeoplePicker/components/Component.types.ts +++ b/PeoplePicker/PeoplePicker/components/Component.types.ts @@ -12,7 +12,7 @@ export interface CanvasPeoplePickerProps { visible?: boolean; isPickerDisabled: boolean; showSecondaryText: boolean; - peoplepickerType: string; + peoplePickerType: string; error?: boolean; componentRef?: IRefObject>; onResize?: (width: number, height: number) => void; @@ -24,7 +24,7 @@ export interface CanvasPeoplePickerProps { onBlur: () => void; minimumFilterLength: number; keepTypingMessage: string; - noresultfoundText: string; + noResultFoundText: string; suggestionsHeaderText: string; hintText: string; maxPeople: number; diff --git a/PeoplePicker/PeoplePicker/components/DatasetMapping.ts b/PeoplePicker/PeoplePicker/components/DatasetMapping.ts index 2fe0cc20..92389698 100644 --- a/PeoplePicker/PeoplePicker/components/DatasetMapping.ts +++ b/PeoplePicker/PeoplePicker/components/DatasetMapping.ts @@ -71,7 +71,7 @@ export function getPersonaPresence(personaPresence: string): PersonaPresence { } } -export function getDataSetfromPersona(selectedPeople: IPersonaProps[]): ICustomPersonaProps[] { +export function getDataSetFromPersona(selectedPeople: IPersonaProps[]): ICustomPersonaProps[] { return selectedPeople.map((user) => { return { PersonaKey: user.key, diff --git a/PeoplePicker/PeoplePicker/components/Peoplepicker.tsx b/PeoplePicker/PeoplePicker/components/Peoplepicker.tsx index 6d82d49a..6f116019 100644 --- a/PeoplePicker/PeoplePicker/components/Peoplepicker.tsx +++ b/PeoplePicker/PeoplePicker/components/Peoplepicker.tsx @@ -26,10 +26,10 @@ export const CanvasPeoplePicker = React.memo((props: CanvasPeoplePickerProps) => filterSuggestions, minimumFilterLength, keepTypingMessage, - noresultfoundText, + noResultFoundText, isPickerDisabled, error, - peoplepickerType, + peoplePickerType, accessibilityLabel, suggestionsHeaderText, componentRef, @@ -45,16 +45,16 @@ export const CanvasPeoplePicker = React.memo((props: CanvasPeoplePickerProps) => const [peopleList, setPeopleList] = React.useState(suggestedPeople); const prevSelectedPeople = usePrevious(defaultSelected); - const prevpeopleList = usePrevious(suggestedPeople); + const prevPeopleList = usePrevious(suggestedPeople); const [searchTerm, setSearchTerm] = React.useState(''); - const [pickerKey, setPickerKey] = React.useState(peoplepickerType.split(' ')[0]); + const [pickerKey, setPickerKey] = React.useState(peoplePickerType.split(' ')[0]); const suggestionProps: IBasePickerSuggestionsProps = { suggestionsHeaderText: suggestionsHeaderText, mostRecentlyUsedHeaderText: suggestionsHeaderText, noResultsFoundText: searchTerm && minimumFilterLength && searchTerm.length < minimumFilterLength ? keepTypingMessage - : noresultfoundText, + : noResultFoundText, loadingText: 'Loading', showRemoveButtons: true, suggestionsAvailableAlertText: 'People Picker Suggestions available', @@ -80,10 +80,10 @@ export const CanvasPeoplePicker = React.memo((props: CanvasPeoplePickerProps) => // To re-render the existing component during pre-selected members change setPickerKey(pickerKey.concat('_1')); } - if (prevpeopleList !== suggestedPeople) { + if (prevPeopleList !== suggestedPeople) { setPeopleList(suggestedPeople); } - }, [onPersonSelect, pickerKey, suggestedPeople, defaultSelected, prevSelectedPeople, prevpeopleList]); + }, [onPersonSelect, pickerKey, suggestedPeople, defaultSelected, prevSelectedPeople, prevPeopleList]); const rootStyle = React.useMemo(() => { // This is needed for custom pages to ensure the People Picker grows to the full width @@ -147,7 +147,7 @@ export const CanvasPeoplePicker = React.memo((props: CanvasPeoplePickerProps) => onPersonSelect([]); } }; - const peoplepickerProps: IPeoplePickerProps = { + const peoplePickerProps: IPeoplePickerProps = { // eslint-disable-next-line react/jsx-no-bind onResolveSuggestions: filterSuggestedUsers, getTextFromItem: getTextFromItem, @@ -173,17 +173,17 @@ export const CanvasPeoplePicker = React.memo((props: CanvasPeoplePickerProps) => onChange: onChange, } as IPeoplePickerProps; return ( - + {(() => { - switch (peoplepickerType.toLowerCase()) { + switch (peoplePickerType.toLowerCase()) { case 'normal people picker': - return ; + return ; case 'list people picker': - return ; + return ; case 'compact people picker': - return ; + return ; default: - return ; + return ; } })()} diff --git a/PeoplePicker/PeoplePicker/css/PeoplePicker.css b/PeoplePicker/PeoplePicker/css/PeoplePicker.css index 89112d6c..5c7a4c86 100644 --- a/PeoplePicker/PeoplePicker/css/PeoplePicker.css +++ b/PeoplePicker/PeoplePicker/css/PeoplePicker.css @@ -1,7 +1,7 @@ /* Error Condition Overrides*/ -.PowerCATPeoplepicker .error-condition .ms-BasePicker-text, -.PowerCATPeoplepicker .error-condition .ms-BasePicker-text:hover, -.PowerCATPeoplepicker .error-condition .ms-BasePicker-text::after { +.PowerCATPeoplePicker .error-condition .ms-BasePicker-text, +.PowerCATPeoplePicker .error-condition .ms-BasePicker-text:hover, +.PowerCATPeoplePicker .error-condition .ms-BasePicker-text::after { border-color: #BA3D22 !important; } diff --git a/PeoplePicker/PeoplePicker/index.ts b/PeoplePicker/PeoplePicker/index.ts index 50c2e8e1..b7aad05a 100644 --- a/PeoplePicker/PeoplePicker/index.ts +++ b/PeoplePicker/PeoplePicker/index.ts @@ -1,12 +1,13 @@ import { IInputs, IOutputs } from './generated/ManifestTypes'; import { CanvasPeoplePicker } from './components/Peoplepicker'; import * as React from 'react'; -import { ManifestPropertyNames, SuggestionColumns, InputProperties, InputEvents } from './ManifestConstants'; +import { ManifestPropertyNames, InputProperties, InputEvents } from './ManifestConstants'; import { CanvasPeoplePickerProps, ICustomPersonaProps, IPropBag } from './components/Component.types'; import { IPersonaProps, IBasePicker } from '@fluentui/react'; import { getPersonaFromDataset, getSuggestionFromDataset } from './components/DatasetMapping'; import { PersonaSchema, IOutputSchemaMap } from './components/PersonaSchema'; -import { getDataSetfromPersona } from './components/DatasetMapping'; +import { getDataSetFromPersona as getDataSetFromPersona } from './components/DatasetMapping'; +import { debounce } from 'debounce'; interface CustomControl extends ComponentFramework.ReactControl { /** @@ -19,6 +20,13 @@ interface CustomControl extends ComponentFramework.ReactContr getOutputSchema?(): Promise; } +// Time to wait for either a loading signal or unchanged suggestions (milliseconds) +const SUGGESTIONS_CHANGE_TIMEOUT = 5000; +// Debounce Search event time (milliseconds) to allow for updated suggestions to be received within debounce window +const SUGGESTIONS_DEBOUNCE = 500; +// Debounce Notify search event changed (milliseconds) to prevent constant re-triggering of search +const NOTIFY_OUTPUT_CHANGED_DEBOUNCE = 500; + export class PeoplePicker implements CustomControl { context: IPropBag; private notifyOutputChanged: () => void; @@ -30,8 +38,9 @@ export class PeoplePicker implements CustomControl { searchText: string; previousSearchText: string; suggestionsFilterPending?: (suggestions: IPersonaProps[]) => void; - refreshSuggestions: boolean; resolve?: (selectedPeople: IPersonaProps[]) => void; + suggestionsLoading?: boolean; + waitingForSuggestions?: boolean; /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. @@ -42,6 +51,8 @@ export class PeoplePicker implements CustomControl { */ public init(context: IPropBag, notifyOutputChanged: () => void): void { this.notifyOutputChanged = notifyOutputChanged; + this.resolveSuggestions = debounce(this.resolveSuggestions, SUGGESTIONS_DEBOUNCE); + this.notifySearchChanged = debounce(this.notifySearchChanged, NOTIFY_OUTPUT_CHANGED_DEBOUNCE); this.context = context; this.context.mode.trackContainerResize(true); this.context.parameters.Suggestions.paging.setPageSize(500); @@ -53,43 +64,6 @@ export class PeoplePicker implements CustomControl { this.notifyOutputChanged(); }; - // Determining the final result by comparing it with SearchText - isFinalResult = (record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord): boolean => { - const suggestionKey = ((record.getValue(SuggestionColumns.SuggestionKey) as string) ?? '').toLowerCase(); - const suggestionName = ((record.getValue(SuggestionColumns.SuggestionName) as string) ?? '').toLowerCase(); - const suggestionRole = ((record.getValue(SuggestionColumns.SuggestionRole) as string) ?? '').toLowerCase(); - const currentSearchText = this.searchText.toLowerCase(); - return ( - suggestionKey.indexOf(currentSearchText) > -1 || - suggestionName.indexOf(currentSearchText) > -1 || - suggestionRole.indexOf(currentSearchText) > -1 - ); - }; - - // To check if the Dataset is the latest by matching searchtext & To determine whether to resolve the promise - checkforLatestDataset = (maxRetries: number): void => { - let peopleListDataset = this.context.parameters.Suggestions; - const sortedIDArray = peopleListDataset.sortedRecordIds; - maxRetries === 0 - ? //return empty result as dataset is assumed empty - this.returnPeople(this.resolve, true) - : setTimeout(() => { - maxRetries = maxRetries - 1; - if (sortedIDArray.length > 0) { - const record = peopleListDataset.records[sortedIDArray[0]]; - if (this.isFinalResult(record)) { - peopleListDataset = this.context.parameters.Suggestions; - this.suggestedPeople = getSuggestionFromDataset(peopleListDataset); - this.returnPeople(this.resolve); - } else { - this.checkforLatestDataset(maxRetries); - } - } else { - this.checkforLatestDataset(maxRetries); - } - }, 50); - }; - /** * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc. * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions @@ -97,20 +71,20 @@ export class PeoplePicker implements CustomControl { */ public updateView(context: IPropBag): React.ReactElement { this.context = context; + const suggestions = this.context.parameters.Suggestions; + const personas = this.context.parameters.Personas; + const getPreSelectedMember = - (this.defaultSelected === undefined && this.context.parameters.Personas.sortedRecordIds.length > 0) || + (this.defaultSelected === undefined && personas.sortedRecordIds.length > 0) || context.updatedProperties.indexOf(ManifestPropertyNames.dataset) > -1; if (getPreSelectedMember) { - const selectedpeopleDataset = context.parameters.Personas; - this.defaultSelected = getPersonaFromDataset(selectedpeopleDataset); - } - if (this.refreshSuggestions) { - this.context.events.OnSearch(); - this.refreshSuggestions = false; - // Check for latest dataset with a defined retries - this.checkforLatestDataset(160); + const selectedPeopleDataset = context.parameters.Personas; + this.defaultSelected = getPersonaFromDataset(selectedPeopleDataset); } + + this.handleSuggestionChangeEvents(context, suggestions); + const inputEvent = context.parameters.InputEvent.raw; if ( context.updatedProperties.indexOf(InputProperties.InputEvent) > -1 && @@ -119,6 +93,7 @@ export class PeoplePicker implements CustomControl { // Set focus on input this.ref?.focusInput(); } + const allocatedWidth = parseInt(context.mode.allocatedWidth as unknown as string); const allocatedHeight = parseInt(context.mode.allocatedHeight as unknown as string); const props: CanvasPeoplePickerProps = { @@ -130,12 +105,12 @@ export class PeoplePicker implements CustomControl { error: context.parameters.Error.raw, isPickerDisabled: context.mode.isControlDisabled, showSecondaryText: context.parameters.ShowSecondaryText.raw, - peoplepickerType: context.parameters.PeoplePickerType.raw, + peoplePickerType: context.parameters.PeoplePickerType.raw, defaultSelected: this.defaultSelected, componentRef: this.componentRefCallback, accessibilityLabel: context.parameters.AccessibilityLabel.raw ?? '', minimumFilterLength: context.parameters.MinimumSearchTermLength.raw ?? 2, - noresultfoundText: context.parameters.NoResultFoundMesage.raw ?? '', + noResultFoundText: context.parameters.NoResultFoundMessage.raw ?? '', keepTypingMessage: context.parameters.SearchTermToShortMessage.raw ?? '', filterSuggestions: this.filterSuggestions, onPersonSelect: this.onPersonSelect, @@ -149,6 +124,77 @@ export class PeoplePicker implements CustomControl { return React.createElement(CanvasPeoplePicker, props); } + private handleSuggestionChangeEvents( + context: IPropBag, + suggestions: ComponentFramework.PropertyTypes.DataSet, + ) { + if (this.resolve) { + // Stage 1 - Search Text has been updated so trigger the Search event + if (context.updatedProperties.indexOf(ManifestPropertyNames.SearchText) > -1) { + // Wait for suggestions to load + this.waitingForSuggestions = true; + this.suggestionsLoading = false; + // Raise search event to invoke any search logic. + // Suggestions may also be directly filtered in the Suggestions dataset + this.raiseSearchEvent(); + // Start debounce to resolve if no change to suggestions after the timeout + this.suggestionsTimeout(); + this.resolveSuggestions.clear(); + } + + // Stage 2 - If the search requires a network operation, it will go into the loading state + if (this.waitingForSuggestions && suggestions.loading) { + // Loading happens when a network operation takes place + this.suggestionsLoading = true; + // Stop any pending resolve because a new dataset is now pending + this.resolveSuggestions.clear(); + // Start suggestions timeout again + this.suggestionsTimeout(); + } + + // Stage 3 - When the suggestions are updated, and they are not loading, resolve the suggestions + if ( + (this.waitingForSuggestions || this.suggestionsLoading) && + context.updatedProperties.indexOf(ManifestPropertyNames.Suggestions_dataset) > -1 && + !suggestions.loading + ) { + // Resolve suggestions unless we get another set of suggestions within the debounce window + this.suggestionsTimeout.clear(); + this.resolveSuggestions(); + } + } + } + + suggestionsTimeout = debounce((): void => { + // If a search term change has triggered the 'waiting for suggestions' state + // but the suggestions have not either been set to loading + // or have changed, then it means that the results are not changed + // In this situation Power Apps does not trigger a dataset change updateView + // and so we timeout and show the previous results + if (this.waitingForSuggestions && this.resolve) { + this.previousSearchText = ''; + this.resolveSuggestions(); + } + }, SUGGESTIONS_CHANGE_TIMEOUT); + + resolveSuggestions = debounce((): void => { + // Resolve suggestions + const suggestions = this.context.parameters.Suggestions; + this.suggestionsLoading = false; + this.waitingForSuggestions = false; + this.suggestedPeople = getSuggestionFromDataset(suggestions); + if (this.resolve) this.resolve(this.suggestedPeople); + this.resolve = undefined; + }, SUGGESTIONS_DEBOUNCE); + + raiseSearchEvent = debounce( + (): void => { + this.context.events.OnSearch(); + }, + NOTIFY_OUTPUT_CHANGED_DEBOUNCE, + true, + ); + onBlur = (): void => { this.context.events.OnBlur(); }; @@ -158,7 +204,7 @@ export class PeoplePicker implements CustomControl { }; onPersonSelect = (people: IPersonaProps[]): void => { - this.selectedPeople = getDataSetfromPersona(people); + this.selectedPeople = getDataSetFromPersona(people); this.notifyOutputChanged(); }; @@ -173,9 +219,7 @@ export class PeoplePicker implements CustomControl { // Notify that the search term has changed - this in turn will filter the suggestions dataset if (this.previousSearchText !== this.searchText) { this.previousSearchText = this.searchText; - this.refreshSuggestions = true; - this.notifyOutputChanged(); - this.resolve = resolve; + this.notifySearchChanged(resolve); } else { // The search has not changed, so return the previous suggestions resolve(this.suggestedPeople); @@ -183,9 +227,11 @@ export class PeoplePicker implements CustomControl { }); }; - // eslint-disable-next-line - returnPeople = (resolve: any, returnEmpty = false): IPersonaProps[] => { - return resolve(returnEmpty ? [] : this.suggestedPeople); + notifySearchChanged = (resolve: (selectedPeople: IPersonaProps[]) => void): void => { + this.resolve = resolve; + this.notifyOutputChanged(); + // Start debounce to resolve if no change to suggestions after the timeout + this.suggestionsTimeout(); }; componentRefCallback = (ref: IBasePicker | null): void => { diff --git a/PeoplePicker/PeoplePicker/strings/PeoplePicker.1033.resx b/PeoplePicker/PeoplePicker/strings/PeoplePicker.1033.resx index 34af6819..602ec7a8 100644 --- a/PeoplePicker/PeoplePicker/strings/PeoplePicker.1033.resx +++ b/PeoplePicker/PeoplePicker/strings/PeoplePicker.1033.resx @@ -130,10 +130,10 @@ The message to display if the search term is less than the 'Minimum Search Term Length' parameter - e.g. 'Keep typing' - + No result found message - + The message to display when no suggestions match the search term diff --git a/PeoplePicker/package-lock.json b/PeoplePicker/package-lock.json index d84be60d..06b749ac 100644 --- a/PeoplePicker/package-lock.json +++ b/PeoplePicker/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fluentui/react": "8.29.0", "@react-hook/resize-observer": "^1.2.5", + "debounce": "^1.2.1", "react": "16.8.6", "react-dom": "16.8.6" }, @@ -17,6 +18,7 @@ "@fluentui/example-data": "^8.4.1", "@fluentui/jest-serializer-merge-styles": "^8.0.17", "@fluentui/react-hooks": "^8.6.4", + "@types/debounce": "^1.2.1", "@types/enzyme": "^3.10.11", "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^27.4.0", @@ -1711,6 +1713,12 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "dev": true }, + "node_modules/@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "node_modules/@types/enzyme": { "version": "3.10.12", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.12.tgz", @@ -3511,6 +3519,11 @@ "node": ">=10" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -12228,6 +12241,12 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "dev": true }, + "@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "@types/enzyme": { "version": "3.10.12", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.12.tgz", @@ -13649,6 +13668,11 @@ "whatwg-url": "^8.0.0" } }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/PeoplePicker/package.json b/PeoplePicker/package.json index cc9b89e0..8bd68dc3 100644 --- a/PeoplePicker/package.json +++ b/PeoplePicker/package.json @@ -15,6 +15,7 @@ "dependencies": { "@fluentui/react": "8.29.0", "@react-hook/resize-observer": "^1.2.5", + "debounce": "^1.2.1", "react": "16.8.6", "react-dom": "16.8.6" }, @@ -22,6 +23,7 @@ "@fluentui/example-data": "^8.4.1", "@fluentui/jest-serializer-merge-styles": "^8.0.17", "@fluentui/react-hooks": "^8.6.4", + "@types/debounce": "^1.2.1", "@types/enzyme": "^3.10.11", "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^27.4.0", diff --git a/PeoplePicker/readme.md b/PeoplePicker/readme.md index 4cd71566..cfb6bc6d 100644 --- a/PeoplePicker/readme.md +++ b/PeoplePicker/readme.md @@ -41,7 +41,7 @@ The control accepts the following properties: - **SuggestionOOF** - True or False, Based on whether the persona if 'Out of Office' or not. - **PeoplePickerType** - Type of `PeoplePicker` to be used. - **MaxPeople** - Maximum number of user(s) to be allowed for selection - - **NoResultFoundMesage** - Message to be shown if no results are found based on the specified search text. + - **NoResultFoundMessage** - Message to be shown if no results are found based on the specified search text. - **MinimumSearchTermLength** - Minimum search term length to be entered before providing the suggestions. - **SearchTermToShortMessage** - Custom message to be shown when the search text is less than *MinimumSearchTermLength*. - **Error** - To highlighting the people picker in red to represent that it has certain error which required validation. @@ -92,7 +92,7 @@ Step 2) Setup the Suggestions_Items Property by specifying this UserCollection. UserCollection ``` -Note : In Step 1, we are making a consequentive request to get UserPhoto. This leads to increase in time of fetching the results. If you want to decrease the fetching time and can compromise on not showing the profile images, then use the below code instead of UserCollection & Skip Step 1) altogether. +Note : In Step 1, we are making a consecutive request to get UserPhoto. This leads to increase in time of fetching the results. If you want to decrease the fetching time and can compromise on not showing the profile images, then use the below code instead of UserCollection & Skip Step 1) altogether. ```Power Fx Office365Users.SearchUser({searchTerm:Self.SearchText,top:500})