[react-search-refiners] (#465)
* Upgraded to SPFx 1.4.1 * Added the ability to set you own refiners labels in the filters panel. * Replaced the `pushState` method by the SPFx `eventAggregator` for the communication between the search box and results web parts. * CSS improvements * Added an option to show the results count
This commit is contained in:
parent
5b0a88f331
commit
74fd5ca5ce
|
@ -10,7 +10,7 @@ This sample shows you how to build user friendly SharePoint search experiences u
|
|||
An associated [blog post](http://thecollaborationcorner.com/2017/10/16/build-dynamic-sharepoint-search-experiences-with-refiners-and-paging-with-spfx-office-ui-fabric-and-pnp-js-library/) is available to give you more details about this sample implementation.
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-1.4.0-green.svg)
|
||||
![drop](https://img.shields.io/badge/drop-1.4.1-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
|
@ -56,10 +56,16 @@ Setting | Description
|
|||
Search query keywords | The search query in KQL format. You can use search query variables (See this [post](http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html) to know which ones are allowed).
|
||||
Query template | The search query template in KQL format. You can use search variables here (like Path:{Site}).
|
||||
Selected properties | The search managed properties to retrieve. You can use these proeprties then in the code like this (`item.property_name`). (See the *Tile.tsx* file) .
|
||||
Refiners | The search managed properties to use as refiners. Make sure these are refinable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the refnement panel.
|
||||
Refiners | The search managed properties to use as refiners. Make sure these are refinable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the refnement panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",...
|
||||
Number of items to retrieve per page | Quite explicit. The paging behavior is done directly by the search API (See the *SearchDataProvider.ts* file), not by the code on post-render.
|
||||
Show paging | Indicates whether or not the component should show the paging control at the bottom.
|
||||
|
||||
## Search Box/Search Results communication
|
||||
|
||||
The communication between the two web parts is done using the default SPFx `eventAggregator` property (still in alpha as of march 2018). However, this link can be updated to use the concept shown in the [react-rxjs-event-emitter](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-rxjs-event-emitter) example().
|
||||
|
||||
Checkout this [article](https://blog.velingeorgiev.com/sharepoint-framework-publish-subscribe-event-messaging) by Velin Georgiev to get more information.
|
||||
|
||||
## Features
|
||||
This Web Part illustrates the following concepts on top of the SharePoint Framework:
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"solution": {
|
||||
"name": "PnP - Search Web Parts",
|
||||
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"version": "1.1.0.1",
|
||||
"version": "1.1.0.2",
|
||||
"includeClientSideAssets": true
|
||||
},
|
||||
"paths": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,35 +11,37 @@
|
|||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "~1.4.0",
|
||||
"@microsoft/sp-lodash-subset": "~1.4.0",
|
||||
"@microsoft/sp-webpart-base": "~1.4.0",
|
||||
"@pnp/spfx-controls-react": "1.2.1",
|
||||
"@pnp/spfx-property-controls": "1.3.0",
|
||||
"@microsoft/sp-core-library": "~1.4.1",
|
||||
"@microsoft/sp-lodash-subset": "~1.4.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
|
||||
"@microsoft/sp-webpart-base": "~1.4.1",
|
||||
"@pnp/common": "1.0.3",
|
||||
"@pnp/logging": "1.0.3",
|
||||
"@pnp/odata": "1.0.3",
|
||||
"@pnp/sp": "1.0.3",
|
||||
"@pnp/spfx-controls-react": "1.2.3",
|
||||
"@pnp/spfx-property-controls": "1.4.2",
|
||||
"@types/react": "15.6.6",
|
||||
"@types/react-addons-shallow-compare": "0.14.17",
|
||||
"@types/react-addons-test-utils": "0.14.15",
|
||||
"@types/react-addons-update": "0.14.14",
|
||||
"@types/react-dom": "15.5.6",
|
||||
"@types/sharepoint": "2013.1.9",
|
||||
"@types/webpack-env": ">=1.12.1 <1.14.0",
|
||||
"immutability-helper": "2.4.0",
|
||||
"lodash-es": "4.17.4",
|
||||
"moment": "2.21.0",
|
||||
"office-ui-fabric-react": "4.40.2-hotfix.1",
|
||||
"office-ui-fabric-react": "5.21.0",
|
||||
"react": "15.6.2",
|
||||
"react-custom-scrollbars": "4.1.2",
|
||||
"react-dom": "15.6.2",
|
||||
"react-js-pagination": "3.0.0",
|
||||
"sp-pnp-js": "3.0.3",
|
||||
"@microsoft/sp-office-ui-fabric-core": "~1.4.0"
|
||||
"react-js-pagination": "3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "~1.4.0",
|
||||
"@microsoft/sp-module-interfaces": "~1.4.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.4.0",
|
||||
"gulp": "~3.9.1",
|
||||
"@microsoft/sp-build-web": "~1.4.1",
|
||||
"@microsoft/sp-module-interfaces": "~1.4.1",
|
||||
"@microsoft/sp-webpart-workbench": "~1.4.1",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/jquery": "2.0.48",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0",
|
||||
"ajv": "~5.2.2"
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,10 +29,9 @@ interface ISearchDataProvider {
|
|||
|
||||
/**
|
||||
* Perfoms a search query.
|
||||
* @returns ISearchResults object. Use the "RelevantResults" property to acces results proeprties (returned as key/value pair object => item.[<Managed property name>])
|
||||
* @returns ISearchResults object. Use the "RelevantResults" property to access results properties (returned as key/value pair object => item.[<Managed property name>])
|
||||
*/
|
||||
search(kqlQuery: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults>;
|
||||
|
||||
}
|
||||
|
||||
export default ISearchDataProvider;
|
|
@ -1,6 +1,8 @@
|
|||
import ISearchDataProvider from "./ISearchDataProvider";
|
||||
import { ISearchResults, ISearchResult, IRefinementResult, IRefinementValue, IRefinementFilter } from "../models/ISearchResult";
|
||||
import pnp, { ConsoleListener, Logger, LogLevel, SearchQuery, SearchQueryBuilder, SearchResults, setup, Web, Sort, SortDirection } from "sp-pnp-js";
|
||||
import { sp, SearchQuery, SearchQueryBuilder, SearchResults, SPRest, Web, Sort, SortDirection } from "@pnp/sp";
|
||||
import { PnPClientStorage, Util } from "@pnp/common";
|
||||
import { Logger, LogLevel, ConsoleListener } from "@pnp/logging";
|
||||
import { IWebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { Text, JsonUtilities, UrlUtilities } from "@microsoft/sp-core-library";
|
||||
import sortBy from "lodash-es/sortBy";
|
||||
|
@ -8,7 +10,6 @@ import groupBy from 'lodash-es/groupBy';
|
|||
import mapValues from 'lodash-es/mapValues';
|
||||
import mapKeys from "lodash-es/mapKeys";
|
||||
import * as moment from "moment";
|
||||
import { SPRest } from "sp-pnp-js/lib/sharepoint/rest";
|
||||
|
||||
class SearchDataProvider implements ISearchDataProvider {
|
||||
|
||||
|
@ -48,7 +49,7 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
// To limit the payload size, we set odata=nometadata
|
||||
// We just need to get list items here
|
||||
// We use a local configuration to avoid conflicts with other Web Parts
|
||||
this._localPnPSetup= pnp.sp.configure({
|
||||
this._localPnPSetup= sp.configure({
|
||||
headers: {
|
||||
Accept: "application/json; odata=nometadata",
|
||||
},
|
||||
|
|
|
@ -18,7 +18,9 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<ISearchBoxProps > = React.createElement(
|
||||
SearchBox, { });
|
||||
SearchBox, {
|
||||
eventAggregator: this.context.eventAggregator,
|
||||
});
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
import { IEventAggregator } from "@microsoft/sp-webpart-base";
|
||||
|
||||
export interface ISearchBoxProps {
|
||||
eventAggregator: IEventAggregator;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { SearchBox } from "office-ui-fabric-react/lib/SearchBox";
|
|||
import { Text } from "@microsoft/sp-core-library";
|
||||
import * as strings from 'SearchBoxWebPartStrings';
|
||||
|
||||
export default class SearchBoxContainer extends React.Component<ISearchBoxProps, {}> {
|
||||
export default class SearchBoxContainer extends React.Component<ISearchBoxProps, null> {
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
@ -14,10 +14,12 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxProps,
|
|||
}
|
||||
|
||||
public render(): React.ReactElement<ISearchBoxProps> {
|
||||
return (
|
||||
|
||||
return (
|
||||
<SearchBox
|
||||
onSearch={ this.onSearch }
|
||||
placeholder={ strings.SearchInputPlaceholder }
|
||||
onClear={ () => { this.onSearch("*") }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -27,10 +29,11 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxProps,
|
|||
* @param queryText The query text entered by the user
|
||||
*/
|
||||
public onSearch(queryText: string) {
|
||||
const url = new URLSearchParams();
|
||||
url.append("k", queryText);
|
||||
|
||||
// The data parameter wil be caught by the search results WP
|
||||
history.pushState({ k: queryText}, '', Text.format("#{0}", url.toString()));
|
||||
this.props.eventAggregator.raiseEvent("search:newQueryKeywords", {
|
||||
data: queryText,
|
||||
sourceId: "SearchBoxQuery",
|
||||
targetId: "SearchResults"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ export interface ISearchResultsWebPartProps {
|
|||
showPaging: boolean;
|
||||
showFileIcon: boolean;
|
||||
showCreatedDate: boolean;
|
||||
showResultsCount: boolean;
|
||||
useSearchBoxQuery: boolean;
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@
|
|||
"properties": {
|
||||
"queryKeywords": "",
|
||||
"queryTemplate": "{searchTerms} Path:{Site}",
|
||||
"refiners": "Created",
|
||||
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL",
|
||||
"refiners": "Created:\"Created Date\",Size:\"Size of the file\"",
|
||||
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL",
|
||||
"enableQueryRules": false,
|
||||
"maxResultsCount": 10,
|
||||
"showFileIcon": true,
|
||||
|
|
|
@ -6,7 +6,8 @@ import {
|
|||
PropertyPaneSlider,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneToggle
|
||||
PropertyPaneToggle,
|
||||
IEvent
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import * as strings from 'SearchWebPartStrings';
|
||||
|
@ -20,12 +21,20 @@ import * as moment from "moment";
|
|||
import { Placeholder, IPlaceholderProps } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { PropertyPaneCheckbox } from '@microsoft/sp-webpart-base/lib/propertyPane/propertyPaneFields/propertyPaneCheckBox/PropertyPaneCheckbox';
|
||||
import { PropertyPaneHorizontalRule } from '@microsoft/sp-webpart-base/lib/propertyPane/propertyPaneFields/propertyPaneHorizontalRule/PropertyPaneHorizontalRule';
|
||||
import { UrlUtilities, DisplayMode } from "@microsoft/sp-core-library";
|
||||
|
||||
export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsWebPartProps> {
|
||||
|
||||
private _dataProvider: ISearchDataProvider;
|
||||
private _useResultSource: boolean;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this._parseRefiners = this._parseRefiners.bind(this);
|
||||
this.bindSearchQuery = this.bindSearchQuery.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the base onInit() implementation to get the persisted properties to initialize data provider.
|
||||
*/
|
||||
|
@ -41,11 +50,11 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
this._dataProvider = new SearchDataProvider(this.context);
|
||||
}
|
||||
|
||||
// Register an handler to catch search box queries
|
||||
this.bindPushStateEvent();
|
||||
|
||||
this._useResultSource = false;
|
||||
|
||||
// Use the SPFx event aggregator to get the search box query
|
||||
this.context.eventAggregator.subscribeByEventName("search:newQueryKeywords", this.componentId , this.bindSearchQuery);
|
||||
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
|
@ -73,10 +82,11 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
resultSourceId: this.properties.resultSourceId,
|
||||
enableQueryRules: this.properties.enableQueryRules,
|
||||
selectedProperties: this.properties.selectedProperties ? this.properties.selectedProperties.replace(/\s|,+$/g, '').split(",") : [],
|
||||
refiners: this.properties.refiners,
|
||||
refiners: this._parseRefiners(this.properties.refiners),
|
||||
showPaging: this.properties.showPaging,
|
||||
showFileIcon: this.properties.showFileIcon,
|
||||
showCreatedDate: this.properties.showCreatedDate
|
||||
showCreatedDate: this.properties.showCreatedDate,
|
||||
showResultsCount: this.properties.showResultsCount,
|
||||
} as ISearchContainerProps
|
||||
);
|
||||
|
||||
|
@ -92,8 +102,8 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
);
|
||||
|
||||
renderElement = (this.properties.queryKeywords && !this.properties.useSearchBoxQuery) || this.properties.useSearchBoxQuery ? searchContainer : placeholder;
|
||||
|
||||
ReactDom.render(renderElement, this.domElement);
|
||||
|
||||
ReactDom.render(renderElement, this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
|
@ -156,7 +166,7 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
multiline: true,
|
||||
resizable: true,
|
||||
value: this.properties.refiners,
|
||||
deferredValidationTime: 300
|
||||
deferredValidationTime: 300,
|
||||
}),
|
||||
PropertyPaneSlider("maxResultsCount", {
|
||||
label: strings.MaxResultsCount,
|
||||
|
@ -171,6 +181,10 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
{
|
||||
groupName: strings.StylingSettingsGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneToggle("showResultsCount", {
|
||||
label: strings.ShowResultsCountLabel,
|
||||
checked: this.properties.showResultsCount,
|
||||
}),
|
||||
PropertyPaneToggle("showPaging", {
|
||||
label: strings.ShowPagingLabel,
|
||||
checked: this.properties.showPaging,
|
||||
|
@ -182,7 +196,7 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
PropertyPaneToggle("showCreatedDate", {
|
||||
label: strings.ShowCreatedDateLabel,
|
||||
checked: this.properties.showCreatedDate,
|
||||
})
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -207,29 +221,15 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
return "";
|
||||
}
|
||||
|
||||
private bindPushStateEvent() {
|
||||
public bindSearchQuery(eventName: string, eventData: IEvent<any>) {
|
||||
|
||||
// Original source: https://www.eliostruyf.com/check-page-mode-from-within-spfx-extensions
|
||||
|
||||
const _pushState = () => {
|
||||
const _defaultPushState = history.pushState;
|
||||
const _self = this;
|
||||
return function (data: any, title: string, url?: string | null) {
|
||||
|
||||
const currentUrl = new URLSearchParams(url);
|
||||
// We need to call the in context of the component
|
||||
// The "k" parameter is set by the search box component
|
||||
if (_self.properties.useSearchBoxQuery) {
|
||||
_self.properties.queryKeywords = data.k;
|
||||
_self.render();
|
||||
}
|
||||
// Call the original function with the provided arguments
|
||||
// This context is necessary for the context of the history change
|
||||
return _defaultPushState.apply(this, [data, title, url]);
|
||||
};
|
||||
};
|
||||
|
||||
history.pushState = _pushState();
|
||||
if (this.properties.useSearchBoxQuery) {
|
||||
if (eventData.data) {
|
||||
|
||||
this.properties.queryKeywords = eventData.data;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateSourceId(value: string): string {
|
||||
|
@ -246,4 +246,32 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
|
|||
|
||||
return '';
|
||||
}
|
||||
|
||||
private _parseRefiners(rawValue: string) : { [key: string]: string } {
|
||||
|
||||
let refiners = {};
|
||||
|
||||
// Get each configuration
|
||||
let refinerKeyValuePair = rawValue.split(",");
|
||||
|
||||
if (refinerKeyValuePair.length > 0) {
|
||||
refinerKeyValuePair.map((e) => {
|
||||
|
||||
const refinerValues = e.split(":");
|
||||
switch (refinerValues.length) {
|
||||
case 1:
|
||||
// Take the same name as the refiner managed property
|
||||
refiners[refinerValues[0]] = refinerValues[0];
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Trim quotes if present
|
||||
refiners[refinerValues[0]] = refinerValues[1].replace(/^"(.*)"$/, '$1');
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return refiners;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
IGroupDividerProps
|
||||
} from 'office-ui-fabric-react/lib/components/GroupedList/index';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
import { ActionButton } from "office-ui-fabric-react";
|
||||
|
||||
export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> {
|
||||
|
||||
|
@ -53,7 +54,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
|
||||
groups.push({
|
||||
key: i.toString(),
|
||||
name: filter.FilterName,
|
||||
name: this.props.refinersConfiguration[filter.FilterName],
|
||||
count: 1,
|
||||
startIndex: i,
|
||||
isDropEnabled: true,
|
||||
|
@ -114,7 +115,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
|
||||
return (
|
||||
<div>
|
||||
<DefaultButton
|
||||
<ActionButton
|
||||
className="searchWp__filterResultBtn"
|
||||
iconProps={{ iconName: 'Filter' }}
|
||||
text={strings.FilterResultsButtonLabel}
|
||||
|
@ -138,7 +139,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
closeButtonAriaLabel='Close'
|
||||
hasCloseButton={true}
|
||||
headerClassName="filterPanel__header"
|
||||
|
||||
onRenderBody={() => {
|
||||
if (this.props.availableFilters.length > 0) {
|
||||
return (
|
||||
|
|
|
@ -3,6 +3,7 @@ import RefinementFilterOperationCallback from "../../../models/RefinementValueOp
|
|||
|
||||
interface IFilterPanelProps {
|
||||
availableFilters: IRefinementResult[];
|
||||
refinersConfiguration: { [key: string]: string };
|
||||
onUpdateFilters: RefinementFilterOperationCallback;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@ interface ISearchResultsContainerProps {
|
|||
resultSourceId: string;
|
||||
enableQueryRules: boolean;
|
||||
selectedProperties: string[];
|
||||
refiners: string;
|
||||
refiners: { [key: string]: string };
|
||||
showPaging: boolean;
|
||||
showFileIcon: boolean;
|
||||
showCreatedDate: boolean;
|
||||
showResultsCount: boolean;
|
||||
}
|
||||
|
||||
export default ISearchResultsContainerProps;
|
|
@ -3,7 +3,7 @@ import ISearchContainerProps from "./ISearchResultsContainerProps";
|
|||
import ISearchContainerState from "./ISearchResultsContainerState";
|
||||
import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar";
|
||||
import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner";
|
||||
import { Logger, LogLevel } from "sp-pnp-js";
|
||||
import { Logger, LogLevel } from "@pnp/logging";
|
||||
import * as strings from "SearchWebPartStrings";
|
||||
import { ISearchResults, IRefinementFilter } from "../../../models/ISearchResult";
|
||||
import TilesList from "../TilesList/TilesList";
|
||||
|
@ -11,7 +11,8 @@ import "../SearchResultsWebPart.scss";
|
|||
import FilterPanel from "../FilterPanel/FilterPanel";
|
||||
import Paging from "../Paging/Paging";
|
||||
import { Overlay } from "office-ui-fabric-react/lib/Overlay";
|
||||
import { UrlQueryParameterCollection } from "@microsoft/sp-core-library";
|
||||
import { Label } from "office-ui-fabric-react";
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
|
||||
export default class SearchResultsContainer extends React.Component<ISearchContainerProps, ISearchContainerState> {
|
||||
|
||||
|
@ -45,9 +46,11 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
const errorMessage = this.state.errorMessage;
|
||||
const isComponentLoading = this.state.isComponentLoading;
|
||||
|
||||
let wpContent: JSX.Element = null;
|
||||
let renderOverlay = null;
|
||||
let renderWpContent: JSX.Element = null;
|
||||
let renderOverlay: JSX.Element = null;
|
||||
let renderCount: JSX.Element = null;
|
||||
|
||||
|
||||
if (!isComponentLoading && areResultsLoading) {
|
||||
renderOverlay = <div>
|
||||
<Overlay isDarkThemed={false} className="overlay">
|
||||
|
@ -55,26 +58,32 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
</div>;
|
||||
}
|
||||
|
||||
if (this.props.showResultsCount && !this.state.areResultsLoading ) {
|
||||
renderCount = <label dangerouslySetInnerHTML={ {__html: Text.format(strings.CountMessage, this.state.results.TotalRows, this.props.queryKeywords) }}></label>;
|
||||
}
|
||||
|
||||
if (isComponentLoading) {
|
||||
wpContent = <Spinner size={SpinnerSize.large} label={strings.LoadingMessage} />;
|
||||
renderWpContent = <Spinner size={SpinnerSize.large} label={strings.LoadingMessage} />;
|
||||
} else {
|
||||
|
||||
if (hasError) {
|
||||
wpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
|
||||
renderWpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
|
||||
} else {
|
||||
|
||||
if (items.RelevantResults.length === 0) {
|
||||
wpContent =
|
||||
renderWpContent =
|
||||
<div>
|
||||
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} />
|
||||
<div className="searchWp__noresult">{strings.NoResultMessage}</div>
|
||||
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} refinersConfiguration={ this.props.refiners } />
|
||||
<div className="searchWp__count">{ renderCount }</div>
|
||||
<div className="searchWp__noresult">{strings.NoResultMessage}</div>
|
||||
</div>;
|
||||
} else {
|
||||
wpContent =
|
||||
renderWpContent =
|
||||
|
||||
<div>
|
||||
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} />
|
||||
{renderOverlay}
|
||||
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} refinersConfiguration={ this.props.refiners }/>
|
||||
<div className="searchWp__count">{ renderCount }</div>
|
||||
{ renderOverlay }
|
||||
<TilesList items={items.RelevantResults} showFileIcon={this.props.showFileIcon} showCreatedDate={this.props.showCreatedDate} />
|
||||
{this.props.showPaging ?
|
||||
<Paging
|
||||
|
@ -91,7 +100,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
|
||||
return (
|
||||
<div className="searchWp">
|
||||
{wpContent}
|
||||
{renderWpContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -106,7 +115,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
|
||||
this.props.searchDataProvider.selectedProperties = this.props.selectedProperties;
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, this.props.refiners, this.state.selectedFilters, this.state.currentPage);
|
||||
const refinerManagedProperties = Object.keys(this.props.refiners).join(",");
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, this.state.currentPage);
|
||||
|
||||
// Initial filters are just set once for the filter control during the component initialization
|
||||
// By this way, we are be able to select multiple values whithin a specific filter (OR condition). Otherwise, if we pass every time the new filters retrieved from new results,
|
||||
|
@ -136,8 +147,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
public async componentWillReceiveProps(nextProps: ISearchContainerProps) {
|
||||
|
||||
let query = nextProps.queryKeywords + nextProps.searchDataProvider.queryTemplate + nextProps.selectedProperties.join(',');
|
||||
|
||||
// New props are passed to the component when the search query has been changed
|
||||
if (this.props.refiners.toString() !== nextProps.refiners.toString()
|
||||
if (JSON.stringify(this.props.refiners) !== JSON.stringify(nextProps.refiners)
|
||||
|| this.props.maxResultsCount !== nextProps.maxResultsCount
|
||||
|| this.state.lastQuery !== query
|
||||
|| this.props.showFileIcon !== nextProps.showFileIcon
|
||||
|
@ -154,8 +166,11 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
});
|
||||
|
||||
this.props.searchDataProvider.selectedProperties = nextProps.selectedProperties;
|
||||
|
||||
const refinerManagedProperties = Object.keys(nextProps.refiners).join(",");
|
||||
|
||||
// We reset the page number and refinement filters
|
||||
const searchResults = await this.props.searchDataProvider.search(nextProps.queryKeywords, nextProps.refiners, [], 1);
|
||||
const searchResults = await this.props.searchDataProvider.search(nextProps.queryKeywords, refinerManagedProperties, [], 1);
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
|
@ -192,7 +207,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, this.props.refiners, newFilters, 1);
|
||||
const refinerManagedProperties = Object.keys(this.props.refiners).join(",");
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, refinerManagedProperties, newFilters, 1);
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
|
@ -211,7 +228,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, this.props.refiners, this.state.selectedFilters, pageNumber);
|
||||
const refinerManagedProperties = Object.keys(this.props.refiners).join(",");
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, pageNumber);
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
|
|
|
@ -56,12 +56,11 @@
|
|||
}
|
||||
|
||||
&__filterResultBtn {
|
||||
margin: 10px;
|
||||
color: "[theme: themePrimary]";
|
||||
}
|
||||
|
||||
&__selectedFilters {
|
||||
|
||||
margin-left: 10px;
|
||||
padding: 10px;
|
||||
|
||||
label.filter {
|
||||
|
@ -127,6 +126,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterPanel {
|
||||
|
|
|
@ -16,6 +16,7 @@ define([], function() {
|
|||
"ShowPagingLabel": "Show paging",
|
||||
"ShowFileIconLabel": "Show file icons",
|
||||
"ShowCreatedDateLabel": "Show created date",
|
||||
"ShowResultsCountLabel": "Show results count",
|
||||
"NoFilterConfiguredLabel": "No filter configured",
|
||||
"SearchQueryPlaceHolderText": "Search query in KQL format",
|
||||
"EmptyFieldErrorMessage": "This field cannot be empty",
|
||||
|
@ -28,9 +29,9 @@ define([], function() {
|
|||
"UseSearchBoxQueryLabel": "Use search box query",
|
||||
"EnableQueryRulesLabel": "Enable query rules",
|
||||
"StylingSettingsGroupName": "Styling options",
|
||||
"RefinersFieldDescription": "Specifies managed properties used as refiners (ordered comma-separated list).",
|
||||
"RefinersFieldDescription": "Specifies managed properties used as refiners (ordered comma-separated list). You can specify the label by using the following format <Managed Property Name>:\"My friendly name\"",
|
||||
"SelectedPropertiesFieldDescription": "Speficies the properties to retrieve from the search results.",
|
||||
"SearchQueryKeywordsFieldDescription": "Use pre-defined search query keywords to retrieve a static set of results."
|
||||
|
||||
"SearchQueryKeywordsFieldDescription": "Use pre-defined search query keywords to retrieve a static set of results.",
|
||||
"CountMessage": "<b>{0}</b> results for '<em>{1}</em>'"
|
||||
}
|
||||
});
|
|
@ -16,6 +16,7 @@ define([], function() {
|
|||
"ShowPagingLabel": "Afficher la pagination",
|
||||
"ShowFileIconLabel": "Afficher les icônes de fichier",
|
||||
"ShowCreatedDateLabel": "Afficher la date de création",
|
||||
"ShowResultsCountLabel": "Afficher le nombre de résultats",
|
||||
"NoFilterConfiguredLabel": "Aucun filtre configuré",
|
||||
"SearchQueryPlaceHolderText": "Requête de recherche au format KQL",
|
||||
"EmptyFieldErrorMessage": "Ce champ ne peut pas être vide",
|
||||
|
@ -28,8 +29,9 @@ define([], function() {
|
|||
"UseSearchBoxQueryLabel": "Utiliser la requête de la boîte de recherche",
|
||||
"EnableQueryRulesLabel": "Activer les règles de requête",
|
||||
"StylingSettingsGroupName": "Options d'affichage",
|
||||
"RefinersFieldDescription": "Propriétés gerées à utiliser comme filtres (liste ordonnée séparée par une virgule)",
|
||||
"RefinersFieldDescription": "Propriétés gerées à utiliser comme filtres (liste ordonnée séparée par une virgule). Vous pouvez spécifier un label personnalisé en utilisant le format suivant <Nom de la propriété gérée>:\"Nom convivial\"",
|
||||
"SelectedPropertiesFieldDescription": "Propriétés à récupérer des résulats de recherche.",
|
||||
"SearchQueryKeywordsFieldDescription": "Utilisez une requête de recherche prédéfinie pour obtenir un ensemble de résultats statique."
|
||||
"SearchQueryKeywordsFieldDescription": "Utilisez une requête de recherche prédéfinie pour obtenir un ensemble de résultats statique.",
|
||||
"CountMessage": "<b>{0}</b> résultats pour '<em>{1}</em>'"
|
||||
}
|
||||
});
|
|
@ -18,6 +18,7 @@ declare interface ISearchWebPartStrings {
|
|||
ShowPagingLabel: string;
|
||||
ShowFileIconLabel: string;
|
||||
ShowCreatedDateLabel: string;
|
||||
ShowResultsCountLabel: string;
|
||||
NoFilterConfiguredLabel: string;
|
||||
SearchQueryPlaceHolderText: string;
|
||||
EmptyFieldErrorMessage: string;
|
||||
|
@ -30,6 +31,7 @@ declare interface ISearchWebPartStrings {
|
|||
UseSearchBoxQueryLabel: string;
|
||||
EnableQueryRulesLabel: string;
|
||||
StylingSettingsGroupName: string;
|
||||
CountMessage: string;
|
||||
}
|
||||
|
||||
declare module 'SearchWebPartStrings' {
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
],
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
"webpack-env",
|
||||
"sharepoint"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue