[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:
Franck Cornu 2018-04-04 10:53:53 -04:00 committed by Vesa Juvonen
parent 5b0a88f331
commit 74fd5ca5ce
22 changed files with 12148 additions and 1165 deletions

View File

@ -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. 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 ## 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 ## 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). 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}). 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) . 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. 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. 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 ## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework: This Web Part illustrates the following concepts on top of the SharePoint Framework:

View File

@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "PnP - Search Web Parts", "name": "PnP - Search Web Parts",
"id": "890affef-33e0-4d72-bd72-36399e02143b", "id": "890affef-33e0-4d72-bd72-36399e02143b",
"version": "1.1.0.1", "version": "1.1.0.2",
"includeClientSideAssets": true "includeClientSideAssets": true
}, },
"paths": { "paths": {

File diff suppressed because it is too large Load Diff

View File

@ -11,35 +11,37 @@
"test": "gulp test" "test": "gulp test"
}, },
"dependencies": { "dependencies": {
"@microsoft/sp-core-library": "~1.4.0", "@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.0", "@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.0", "@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@pnp/spfx-controls-react": "1.2.1", "@microsoft/sp-webpart-base": "~1.4.1",
"@pnp/spfx-property-controls": "1.3.0", "@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": "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/react-dom": "15.5.6",
"@types/sharepoint": "2013.1.9",
"@types/webpack-env": ">=1.12.1 <1.14.0", "@types/webpack-env": ">=1.12.1 <1.14.0",
"immutability-helper": "2.4.0", "immutability-helper": "2.4.0",
"lodash-es": "4.17.4", "lodash-es": "4.17.4",
"moment": "2.21.0", "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": "15.6.2",
"react-custom-scrollbars": "4.1.2", "react-custom-scrollbars": "4.1.2",
"react-dom": "15.6.2", "react-dom": "15.6.2",
"react-js-pagination": "3.0.0", "react-js-pagination": "3.0.0"
"sp-pnp-js": "3.0.3",
"@microsoft/sp-office-ui-fabric-core": "~1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/sp-build-web": "~1.4.0", "@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.0", "@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.0", "@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0", "@types/chai": ">=3.4.34 <3.6.0",
"@types/jquery": "2.0.48",
"@types/mocha": ">=2.2.33 <2.6.0", "@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2" "ajv": "~5.2.2",
"gulp": "~3.9.1"
} }
} }

View File

@ -29,10 +29,9 @@ interface ISearchDataProvider {
/** /**
* Perfoms a search query. * 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>; search(kqlQuery: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults>;
} }
export default ISearchDataProvider; export default ISearchDataProvider;

View File

@ -1,6 +1,8 @@
import ISearchDataProvider from "./ISearchDataProvider"; import ISearchDataProvider from "./ISearchDataProvider";
import { ISearchResults, ISearchResult, IRefinementResult, IRefinementValue, IRefinementFilter } from "../models/ISearchResult"; 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 { IWebPartContext } from "@microsoft/sp-webpart-base";
import { Text, JsonUtilities, UrlUtilities } from "@microsoft/sp-core-library"; import { Text, JsonUtilities, UrlUtilities } from "@microsoft/sp-core-library";
import sortBy from "lodash-es/sortBy"; import sortBy from "lodash-es/sortBy";
@ -8,7 +10,6 @@ import groupBy from 'lodash-es/groupBy';
import mapValues from 'lodash-es/mapValues'; import mapValues from 'lodash-es/mapValues';
import mapKeys from "lodash-es/mapKeys"; import mapKeys from "lodash-es/mapKeys";
import * as moment from "moment"; import * as moment from "moment";
import { SPRest } from "sp-pnp-js/lib/sharepoint/rest";
class SearchDataProvider implements ISearchDataProvider { class SearchDataProvider implements ISearchDataProvider {
@ -48,7 +49,7 @@ class SearchDataProvider implements ISearchDataProvider {
// To limit the payload size, we set odata=nometadata // To limit the payload size, we set odata=nometadata
// We just need to get list items here // We just need to get list items here
// We use a local configuration to avoid conflicts with other Web Parts // We use a local configuration to avoid conflicts with other Web Parts
this._localPnPSetup= pnp.sp.configure({ this._localPnPSetup= sp.configure({
headers: { headers: {
Accept: "application/json; odata=nometadata", Accept: "application/json; odata=nometadata",
}, },

View File

@ -18,7 +18,9 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
public render(): void { public render(): void {
const element: React.ReactElement<ISearchBoxProps > = React.createElement( const element: React.ReactElement<ISearchBoxProps > = React.createElement(
SearchBox, { }); SearchBox, {
eventAggregator: this.context.eventAggregator,
});
ReactDom.render(element, this.domElement); ReactDom.render(element, this.domElement);
} }

View File

@ -1,2 +1,5 @@
import { IEventAggregator } from "@microsoft/sp-webpart-base";
export interface ISearchBoxProps { export interface ISearchBoxProps {
eventAggregator: IEventAggregator;
} }

View File

@ -5,7 +5,7 @@ import { SearchBox } from "office-ui-fabric-react/lib/SearchBox";
import { Text } from "@microsoft/sp-core-library"; import { Text } from "@microsoft/sp-core-library";
import * as strings from 'SearchBoxWebPartStrings'; import * as strings from 'SearchBoxWebPartStrings';
export default class SearchBoxContainer extends React.Component<ISearchBoxProps, {}> { export default class SearchBoxContainer extends React.Component<ISearchBoxProps, null> {
public constructor() { public constructor() {
super(); super();
@ -14,10 +14,12 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxProps,
} }
public render(): React.ReactElement<ISearchBoxProps> { public render(): React.ReactElement<ISearchBoxProps> {
return ( return (
<SearchBox <SearchBox
onSearch={ this.onSearch } onSearch={ this.onSearch }
placeholder={ strings.SearchInputPlaceholder } 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 * @param queryText The query text entered by the user
*/ */
public onSearch(queryText: string) { public onSearch(queryText: string) {
const url = new URLSearchParams();
url.append("k", queryText);
// The data parameter wil be caught by the search results WP this.props.eventAggregator.raiseEvent("search:newQueryKeywords", {
history.pushState({ k: queryText}, '', Text.format("#{0}", url.toString())); data: queryText,
sourceId: "SearchBoxQuery",
targetId: "SearchResults"
});
} }
} }

View File

@ -9,5 +9,6 @@ export interface ISearchResultsWebPartProps {
showPaging: boolean; showPaging: boolean;
showFileIcon: boolean; showFileIcon: boolean;
showCreatedDate: boolean; showCreatedDate: boolean;
showResultsCount: boolean;
useSearchBoxQuery: boolean; useSearchBoxQuery: boolean;
} }

View File

@ -27,8 +27,8 @@
"properties": { "properties": {
"queryKeywords": "", "queryKeywords": "",
"queryTemplate": "{searchTerms} Path:{Site}", "queryTemplate": "{searchTerms} Path:{Site}",
"refiners": "Created", "refiners": "Created:\"Created Date\",Size:\"Size of the file\"",
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL", "selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL",
"enableQueryRules": false, "enableQueryRules": false,
"maxResultsCount": 10, "maxResultsCount": 10,
"showFileIcon": true, "showFileIcon": true,

View File

@ -6,7 +6,8 @@ import {
PropertyPaneSlider, PropertyPaneSlider,
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
PropertyPaneTextField, PropertyPaneTextField,
PropertyPaneToggle PropertyPaneToggle,
IEvent
} from '@microsoft/sp-webpart-base'; } from '@microsoft/sp-webpart-base';
import { Environment, EnvironmentType } from '@microsoft/sp-core-library'; import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
import * as strings from 'SearchWebPartStrings'; 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 { Placeholder, IPlaceholderProps } from "@pnp/spfx-controls-react/lib/Placeholder";
import { PropertyPaneCheckbox } from '@microsoft/sp-webpart-base/lib/propertyPane/propertyPaneFields/propertyPaneCheckBox/PropertyPaneCheckbox'; 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 { 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> { export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsWebPartProps> {
private _dataProvider: ISearchDataProvider; private _dataProvider: ISearchDataProvider;
private _useResultSource: boolean; 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. * 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); this._dataProvider = new SearchDataProvider(this.context);
} }
// Register an handler to catch search box queries
this.bindPushStateEvent();
this._useResultSource = false; 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(); return super.onInit();
} }
@ -73,10 +82,11 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
resultSourceId: this.properties.resultSourceId, resultSourceId: this.properties.resultSourceId,
enableQueryRules: this.properties.enableQueryRules, enableQueryRules: this.properties.enableQueryRules,
selectedProperties: this.properties.selectedProperties ? this.properties.selectedProperties.replace(/\s|,+$/g, '').split(",") : [], 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, showPaging: this.properties.showPaging,
showFileIcon: this.properties.showFileIcon, showFileIcon: this.properties.showFileIcon,
showCreatedDate: this.properties.showCreatedDate showCreatedDate: this.properties.showCreatedDate,
showResultsCount: this.properties.showResultsCount,
} as ISearchContainerProps } as ISearchContainerProps
); );
@ -156,7 +166,7 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
multiline: true, multiline: true,
resizable: true, resizable: true,
value: this.properties.refiners, value: this.properties.refiners,
deferredValidationTime: 300 deferredValidationTime: 300,
}), }),
PropertyPaneSlider("maxResultsCount", { PropertyPaneSlider("maxResultsCount", {
label: strings.MaxResultsCount, label: strings.MaxResultsCount,
@ -171,6 +181,10 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
{ {
groupName: strings.StylingSettingsGroupName, groupName: strings.StylingSettingsGroupName,
groupFields: [ groupFields: [
PropertyPaneToggle("showResultsCount", {
label: strings.ShowResultsCountLabel,
checked: this.properties.showResultsCount,
}),
PropertyPaneToggle("showPaging", { PropertyPaneToggle("showPaging", {
label: strings.ShowPagingLabel, label: strings.ShowPagingLabel,
checked: this.properties.showPaging, checked: this.properties.showPaging,
@ -182,7 +196,7 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
PropertyPaneToggle("showCreatedDate", { PropertyPaneToggle("showCreatedDate", {
label: strings.ShowCreatedDateLabel, label: strings.ShowCreatedDateLabel,
checked: this.properties.showCreatedDate, checked: this.properties.showCreatedDate,
}) }),
] ]
} }
] ]
@ -207,29 +221,15 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
return ""; return "";
} }
private bindPushStateEvent() { public bindSearchQuery(eventName: string, eventData: IEvent<any>) {
// Original source: https://www.eliostruyf.com/check-page-mode-from-within-spfx-extensions if (this.properties.useSearchBoxQuery) {
if (eventData.data) {
const _pushState = () => { this.properties.queryKeywords = eventData.data;
const _defaultPushState = history.pushState; this.render();
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();
} }
private validateSourceId(value: string): string { private validateSourceId(value: string): string {
@ -246,4 +246,32 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchResultsW
return ''; 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;
}
} }

View File

@ -18,6 +18,7 @@ import {
IGroupDividerProps IGroupDividerProps
} from 'office-ui-fabric-react/lib/components/GroupedList/index'; } from 'office-ui-fabric-react/lib/components/GroupedList/index';
import { Scrollbars } from 'react-custom-scrollbars'; import { Scrollbars } from 'react-custom-scrollbars';
import { ActionButton } from "office-ui-fabric-react";
export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> { export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> {
@ -53,7 +54,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
groups.push({ groups.push({
key: i.toString(), key: i.toString(),
name: filter.FilterName, name: this.props.refinersConfiguration[filter.FilterName],
count: 1, count: 1,
startIndex: i, startIndex: i,
isDropEnabled: true, isDropEnabled: true,
@ -114,7 +115,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
return ( return (
<div> <div>
<DefaultButton <ActionButton
className="searchWp__filterResultBtn" className="searchWp__filterResultBtn"
iconProps={{ iconName: 'Filter' }} iconProps={{ iconName: 'Filter' }}
text={strings.FilterResultsButtonLabel} text={strings.FilterResultsButtonLabel}
@ -138,7 +139,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
closeButtonAriaLabel='Close' closeButtonAriaLabel='Close'
hasCloseButton={true} hasCloseButton={true}
headerClassName="filterPanel__header" headerClassName="filterPanel__header"
onRenderBody={() => { onRenderBody={() => {
if (this.props.availableFilters.length > 0) { if (this.props.availableFilters.length > 0) {
return ( return (

View File

@ -3,6 +3,7 @@ import RefinementFilterOperationCallback from "../../../models/RefinementValueOp
interface IFilterPanelProps { interface IFilterPanelProps {
availableFilters: IRefinementResult[]; availableFilters: IRefinementResult[];
refinersConfiguration: { [key: string]: string };
onUpdateFilters: RefinementFilterOperationCallback; onUpdateFilters: RefinementFilterOperationCallback;
} }

View File

@ -7,10 +7,11 @@ interface ISearchResultsContainerProps {
resultSourceId: string; resultSourceId: string;
enableQueryRules: boolean; enableQueryRules: boolean;
selectedProperties: string[]; selectedProperties: string[];
refiners: string; refiners: { [key: string]: string };
showPaging: boolean; showPaging: boolean;
showFileIcon: boolean; showFileIcon: boolean;
showCreatedDate: boolean; showCreatedDate: boolean;
showResultsCount: boolean;
} }
export default ISearchResultsContainerProps; export default ISearchResultsContainerProps;

View File

@ -3,7 +3,7 @@ import ISearchContainerProps from "./ISearchResultsContainerProps";
import ISearchContainerState from "./ISearchResultsContainerState"; import ISearchContainerState from "./ISearchResultsContainerState";
import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar"; import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar";
import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner"; 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 * as strings from "SearchWebPartStrings";
import { ISearchResults, IRefinementFilter } from "../../../models/ISearchResult"; import { ISearchResults, IRefinementFilter } from "../../../models/ISearchResult";
import TilesList from "../TilesList/TilesList"; import TilesList from "../TilesList/TilesList";
@ -11,7 +11,8 @@ import "../SearchResultsWebPart.scss";
import FilterPanel from "../FilterPanel/FilterPanel"; import FilterPanel from "../FilterPanel/FilterPanel";
import Paging from "../Paging/Paging"; import Paging from "../Paging/Paging";
import { Overlay } from "office-ui-fabric-react/lib/Overlay"; 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> { export default class SearchResultsContainer extends React.Component<ISearchContainerProps, ISearchContainerState> {
@ -45,8 +46,10 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
const errorMessage = this.state.errorMessage; const errorMessage = this.state.errorMessage;
const isComponentLoading = this.state.isComponentLoading; const isComponentLoading = this.state.isComponentLoading;
let wpContent: JSX.Element = null; let renderWpContent: JSX.Element = null;
let renderOverlay = null; let renderOverlay: JSX.Element = null;
let renderCount: JSX.Element = null;
if (!isComponentLoading && areResultsLoading) { if (!isComponentLoading && areResultsLoading) {
renderOverlay = <div> renderOverlay = <div>
@ -55,26 +58,32 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
</div>; </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) { if (isComponentLoading) {
wpContent = <Spinner size={SpinnerSize.large} label={strings.LoadingMessage} />; renderWpContent = <Spinner size={SpinnerSize.large} label={strings.LoadingMessage} />;
} else { } else {
if (hasError) { if (hasError) {
wpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>; renderWpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
} else { } else {
if (items.RelevantResults.length === 0) { if (items.RelevantResults.length === 0) {
wpContent = renderWpContent =
<div> <div>
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} /> <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 className="searchWp__noresult">{strings.NoResultMessage}</div>
</div>; </div>;
} else { } else {
wpContent = renderWpContent =
<div> <div>
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} /> <FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} refinersConfiguration={ this.props.refiners }/>
{renderOverlay} <div className="searchWp__count">{ renderCount }</div>
{ renderOverlay }
<TilesList items={items.RelevantResults} showFileIcon={this.props.showFileIcon} showCreatedDate={this.props.showCreatedDate} /> <TilesList items={items.RelevantResults} showFileIcon={this.props.showFileIcon} showCreatedDate={this.props.showCreatedDate} />
{this.props.showPaging ? {this.props.showPaging ?
<Paging <Paging
@ -91,7 +100,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
return ( return (
<div className="searchWp"> <div className="searchWp">
{wpContent} {renderWpContent}
</div> </div>
); );
} }
@ -106,7 +115,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
this.props.searchDataProvider.selectedProperties = this.props.selectedProperties; 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 // 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, // 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) { public async componentWillReceiveProps(nextProps: ISearchContainerProps) {
let query = nextProps.queryKeywords + nextProps.searchDataProvider.queryTemplate + nextProps.selectedProperties.join(','); let query = nextProps.queryKeywords + nextProps.searchDataProvider.queryTemplate + nextProps.selectedProperties.join(',');
// New props are passed to the component when the search query has been changed // 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.props.maxResultsCount !== nextProps.maxResultsCount
|| this.state.lastQuery !== query || this.state.lastQuery !== query
|| this.props.showFileIcon !== nextProps.showFileIcon || this.props.showFileIcon !== nextProps.showFileIcon
@ -154,8 +166,11 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
}); });
this.props.searchDataProvider.selectedProperties = nextProps.selectedProperties; this.props.searchDataProvider.selectedProperties = nextProps.selectedProperties;
const refinerManagedProperties = Object.keys(nextProps.refiners).join(",");
// We reset the page number and refinement filters // 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({ this.setState({
results: searchResults, results: searchResults,
@ -192,7 +207,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
areResultsLoading: true, 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({ this.setState({
results: searchResults, results: searchResults,
@ -211,7 +228,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
areResultsLoading: true, 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({ this.setState({
results: searchResults, results: searchResults,

View File

@ -56,12 +56,11 @@
} }
&__filterResultBtn { &__filterResultBtn {
margin: 10px; color: "[theme: themePrimary]";
} }
&__selectedFilters { &__selectedFilters {
margin-left: 10px;
padding: 10px; padding: 10px;
label.filter { label.filter {
@ -127,6 +126,10 @@
} }
} }
} }
&__count {
padding: 10px;
}
} }
.filterPanel { .filterPanel {

View File

@ -16,6 +16,7 @@ define([], function() {
"ShowPagingLabel": "Show paging", "ShowPagingLabel": "Show paging",
"ShowFileIconLabel": "Show file icons", "ShowFileIconLabel": "Show file icons",
"ShowCreatedDateLabel": "Show created date", "ShowCreatedDateLabel": "Show created date",
"ShowResultsCountLabel": "Show results count",
"NoFilterConfiguredLabel": "No filter configured", "NoFilterConfiguredLabel": "No filter configured",
"SearchQueryPlaceHolderText": "Search query in KQL format", "SearchQueryPlaceHolderText": "Search query in KQL format",
"EmptyFieldErrorMessage": "This field cannot be empty", "EmptyFieldErrorMessage": "This field cannot be empty",
@ -28,9 +29,9 @@ define([], function() {
"UseSearchBoxQueryLabel": "Use search box query", "UseSearchBoxQueryLabel": "Use search box query",
"EnableQueryRulesLabel": "Enable query rules", "EnableQueryRulesLabel": "Enable query rules",
"StylingSettingsGroupName": "Styling options", "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.", "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>'"
} }
}); });

View File

@ -16,6 +16,7 @@ define([], function() {
"ShowPagingLabel": "Afficher la pagination", "ShowPagingLabel": "Afficher la pagination",
"ShowFileIconLabel": "Afficher les icônes de fichier", "ShowFileIconLabel": "Afficher les icônes de fichier",
"ShowCreatedDateLabel": "Afficher la date de création", "ShowCreatedDateLabel": "Afficher la date de création",
"ShowResultsCountLabel": "Afficher le nombre de résultats",
"NoFilterConfiguredLabel": "Aucun filtre configuré", "NoFilterConfiguredLabel": "Aucun filtre configuré",
"SearchQueryPlaceHolderText": "Requête de recherche au format KQL", "SearchQueryPlaceHolderText": "Requête de recherche au format KQL",
"EmptyFieldErrorMessage": "Ce champ ne peut pas être vide", "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", "UseSearchBoxQueryLabel": "Utiliser la requête de la boîte de recherche",
"EnableQueryRulesLabel": "Activer les règles de requête", "EnableQueryRulesLabel": "Activer les règles de requête",
"StylingSettingsGroupName": "Options d'affichage", "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.", "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>'"
} }
}); });

View File

@ -18,6 +18,7 @@ declare interface ISearchWebPartStrings {
ShowPagingLabel: string; ShowPagingLabel: string;
ShowFileIconLabel: string; ShowFileIconLabel: string;
ShowCreatedDateLabel: string; ShowCreatedDateLabel: string;
ShowResultsCountLabel: string;
NoFilterConfiguredLabel: string; NoFilterConfiguredLabel: string;
SearchQueryPlaceHolderText: string; SearchQueryPlaceHolderText: string;
EmptyFieldErrorMessage: string; EmptyFieldErrorMessage: string;
@ -30,6 +31,7 @@ declare interface ISearchWebPartStrings {
UseSearchBoxQueryLabel: string; UseSearchBoxQueryLabel: string;
EnableQueryRulesLabel: string; EnableQueryRulesLabel: string;
StylingSettingsGroupName: string; StylingSettingsGroupName: string;
CountMessage: string;
} }
declare module 'SearchWebPartStrings' { declare module 'SearchWebPartStrings' {

View File

@ -14,7 +14,8 @@
], ],
"types": [ "types": [
"es6-promise", "es6-promise",
"webpack-env" "webpack-env",
"sharepoint"
], ],
"lib": [ "lib": [
"es5", "es5",

File diff suppressed because it is too large Load Diff