[react-search-refiners] Web Part improvements (#390)
* Added the react-search-refiners folder * Updated pagination + format date for DateTime filters * Added grouped list in the filter panel + fixed some bugs * Added custom scrollbar style * Added a placeholder when the Web Part is not configured * Cleaned code * Miscellaneous fixes before PR. * Added Web Part sample images * Updated images * Adde the link to the associated blog post. * Replaced toggle by checkbox + added query template parameter. * react-search-refiners: CSS fixes * Quick fix * Updated the panel position to the left + added an overlay between each search operation to notify the user * [react-search-refiners] Updated the screenshots + npm packages * Synced the remote branch from the upstream to merge things correctly * [react-search-refiners] Removed useless files generated by the upstream merge * [react-search-refiners] Removed remaining useless files
This commit is contained in:
parent
cce1ff319c
commit
14f6753b62
|
@ -7,8 +7,10 @@ This sample shows you how to build user friendly SharePoint search experiences u
|
|||
<img src="./images/react-search-refiners.gif"/>
|
||||
</p>
|
||||
|
||||
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.3.0-green.svg)
|
||||
![drop](https://img.shields.io/badge/drop-1.3.4-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
|
@ -25,7 +27,7 @@ react-search-refiners | Franck Cornu (MVP Office Development at aequos) - Twitte
|
|||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0 | October 17, 2017 | Initial release
|
||||
1.0 | January 03, 2018 | Initial release
|
||||
|
||||
## Disclaimer
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
@ -49,7 +51,8 @@ The following settings are available in the Web Part property pane:
|
|||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Search query | 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}).
|
||||
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.
|
||||
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.
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"solution": {
|
||||
"name": "PnP - Search and Refiners Web Part",
|
||||
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"version": "1.0.0.1"
|
||||
"version": "1.0.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/pnp-react-search-refiners.sppkg"
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.7 MiB |
|
@ -11,11 +11,11 @@
|
|||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "~1.3.0",
|
||||
"@microsoft/sp-lodash-subset": "~1.3.0",
|
||||
"@microsoft/sp-webpart-base": "~1.3.0",
|
||||
"@pnp/spfx-controls-react": "^1.0.0-beta.6",
|
||||
"@pnp/spfx-property-controls": "1.0.0-beta.2",
|
||||
"@microsoft/sp-core-library": "~1.3.4",
|
||||
"@microsoft/sp-lodash-subset": "~1.3.4",
|
||||
"@microsoft/sp-webpart-base": "~1.3.4",
|
||||
"@pnp/spfx-controls-react": "^1.0.0-beta.8",
|
||||
"@pnp/spfx-property-controls": "1.0.0",
|
||||
"@types/react": "15.0.38",
|
||||
"@types/react-addons-shallow-compare": "0.14.17",
|
||||
"@types/react-addons-test-utils": "0.14.15",
|
||||
|
@ -30,12 +30,12 @@
|
|||
"react-custom-scrollbars": "4.1.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-js-pagination": "3.0.0",
|
||||
"sp-pnp-js": "3.0.1"
|
||||
"sp-pnp-js": "3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "~1.3.0",
|
||||
"@microsoft/sp-module-interfaces": "~1.3.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.3.0",
|
||||
"@microsoft/sp-build-web": "~1.3.4",
|
||||
"@microsoft/sp-module-interfaces": "~1.3.4",
|
||||
"@microsoft/sp-webpart-workbench": "~1.3.4",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0"
|
||||
|
|
|
@ -3,15 +3,26 @@ import { ISearchResults, IRefinementFilter } from "../models/ISearchResult";
|
|||
interface ISearchDataProvider {
|
||||
|
||||
/**
|
||||
* Determines the number of items ot retrieve in search REST requests
|
||||
* Determines the number of items ot retrieve in REST requests
|
||||
*/
|
||||
resultsCount: number;
|
||||
|
||||
/**
|
||||
* Selected managed properties to retrieve when a search query is performed
|
||||
*/
|
||||
selectedProperties: string[];
|
||||
|
||||
/**
|
||||
* Determines the query template to apply in REST requests
|
||||
*/
|
||||
queryTemplate?: string;
|
||||
|
||||
/**
|
||||
* Performs a SharePoint 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>])
|
||||
*/
|
||||
search(kqlQuery: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults>;
|
||||
|
||||
}
|
||||
|
||||
export default ISearchDataProvider;
|
|
@ -15,6 +15,7 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
private _context: IWebPartContext;
|
||||
private _appSearchSettings: SearchQuery;
|
||||
private _selectedProperties: string[];
|
||||
private _queryTemplate: string;
|
||||
|
||||
public get resultsCount(): number { return this._resultsCount; }
|
||||
public set resultsCount(value: number) { this._resultsCount = value; }
|
||||
|
@ -22,6 +23,9 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
public set selectedProperties(value: string[]) { this._selectedProperties = value; }
|
||||
public get selectedProperties(): string[] { return this._selectedProperties; }
|
||||
|
||||
public set queryTemplate(value: string) { this._queryTemplate = value; }
|
||||
public get queryTemplate(): string { return this._queryTemplate; }
|
||||
|
||||
public constructor(webPartContext: IWebPartContext) {
|
||||
this._context = webPartContext;
|
||||
|
||||
|
@ -53,15 +57,16 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
let sortedRefiners: string[] = [];
|
||||
|
||||
// Search paging option is one based
|
||||
let page = pageNumber ? pageNumber: 1;
|
||||
let page = pageNumber ? pageNumber : 1;
|
||||
|
||||
searchQuery.ClientType = "ContentSearchRegular";
|
||||
searchQuery.Querytext = query;
|
||||
|
||||
// To be able to use search query variable according to the current context
|
||||
// http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html
|
||||
searchQuery.QueryTemplate = query ? query : "";;
|
||||
searchQuery.QueryTemplate = this._queryTemplate;
|
||||
|
||||
searchQuery.RowLimit = this._resultsCount;
|
||||
searchQuery.RowLimit = this._resultsCount ? this._resultsCount : 50;
|
||||
searchQuery.SelectProperties = this._selectedProperties;
|
||||
searchQuery.TrimDuplicates = false;
|
||||
|
||||
|
@ -130,7 +135,7 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
});
|
||||
|
||||
// Get the icon source URL
|
||||
this._mapToIcon(result.Filename).then((iconUrl) => {
|
||||
this._mapToIcon(result.Filename ? result.Filename : Text.format(".{0}", result.FileExtension)).then((iconUrl) => {
|
||||
|
||||
result.iconSrc = iconUrl;
|
||||
resolvep1(result);
|
||||
|
@ -150,7 +155,7 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
refiner.Entries.map((item) => {
|
||||
values.push({
|
||||
RefinementCount: item.RefinementCount,
|
||||
RefinementName: this._formatDate(item.RefinementName), //This value will appear in the selected filter bar
|
||||
RefinementName: this._formatDate(item.RefinementName), // This value will appear in the selected filter bar
|
||||
RefinementToken: item.RefinementToken,
|
||||
RefinementValue: this._formatDate(item.RefinementValue), // This value will appear in the filter panel
|
||||
});
|
||||
|
@ -240,7 +245,7 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
return filter.Value.RefinementToken;
|
||||
});
|
||||
|
||||
return refinementFilter.length > 1 ? "or(" + refinementFilter + ")" : refinementFilter.toString();
|
||||
return refinementFilter.length > 1 ? Text.format("or({0})", refinementFilter) : refinementFilter.toString();
|
||||
});
|
||||
|
||||
mapKeys(refinementFilters, (value, key) => {
|
||||
|
@ -265,7 +270,7 @@ class SearchDataProvider implements ISearchDataProvider {
|
|||
|
||||
// Multiple filters
|
||||
case (conditionsCount > 1): {
|
||||
refinementQueryString = "and(" + refinementQueryConditions.toString() + ")";
|
||||
refinementQueryString = Text.format("and({0})", refinementQueryConditions.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export interface ISearchWebPartProps {
|
||||
searchQuery: string;
|
||||
queryKeywords: string;
|
||||
queryTemplate: string;
|
||||
maxResultsCount: number;
|
||||
selectedProperties: string;
|
||||
refiners: string;
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
"description": { "default": "Displays search results with customizable dynamic refiners" },
|
||||
"officeFabricIconFontName": "Search",
|
||||
"properties": {
|
||||
"searchQuery": "",
|
||||
"queryKeywords": "",
|
||||
"queryTemplate": "",
|
||||
"refiners": "",
|
||||
"selectedProperties": "",
|
||||
"maxResultsCount": 10
|
||||
|
|
|
@ -46,23 +46,23 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchWebPartP
|
|||
}
|
||||
|
||||
public render(): void {
|
||||
|
||||
this._dataProvider.resultsCount = this.properties.maxResultsCount;
|
||||
this._dataProvider.selectedProperties = this.properties.selectedProperties ?
|
||||
this.properties.selectedProperties.replace(/\s|,+$/g,'').split(",") :[];
|
||||
|
||||
let renderElement = null;
|
||||
|
||||
// Configure the provider before the query according to our needs
|
||||
this._dataProvider.resultsCount = this.properties.maxResultsCount;
|
||||
this._dataProvider.queryTemplate = this.properties.queryTemplate;
|
||||
|
||||
const searchContainer: React.ReactElement<ISearchContainerProps> = React.createElement(
|
||||
SearchContainer,
|
||||
{
|
||||
dataProvider: this._dataProvider,
|
||||
searchQuery: this.properties.searchQuery,
|
||||
searchDataProvider: this._dataProvider,
|
||||
queryKeywords: this.properties.queryKeywords,
|
||||
maxResultsCount: this.properties.maxResultsCount,
|
||||
selectedProperties: this.properties.selectedProperties,
|
||||
selectedProperties: this.properties.selectedProperties ? this.properties.selectedProperties.replace(/\s|,+$/g,'').split(",") :[],
|
||||
refiners: this.properties.refiners,
|
||||
showPaging: this.properties.showPaging,
|
||||
}
|
||||
} as ISearchContainerProps
|
||||
);
|
||||
|
||||
const placeholder: React.ReactElement<IPlaceholderProps> = React.createElement(
|
||||
|
@ -76,7 +76,7 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchWebPartP
|
|||
}
|
||||
);
|
||||
|
||||
renderElement = this.properties.searchQuery ? searchContainer : placeholder;
|
||||
renderElement = this.properties.queryKeywords ? searchContainer : placeholder;
|
||||
|
||||
ReactDom.render(renderElement, this.domElement);
|
||||
|
||||
|
@ -94,9 +94,17 @@ export default class SearchWebPart extends BaseClientSideWebPart<ISearchWebPartP
|
|||
{
|
||||
groupName: strings.SearchSettingsGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('searchQuery', {
|
||||
label: strings.SearchQueryFieldLabel,
|
||||
value: "Path:{Site}",
|
||||
PropertyPaneTextField('queryKeywords', {
|
||||
label: strings.SearchQueryKeywordsFieldLabel,
|
||||
value: "",
|
||||
multiline: true,
|
||||
resizable: true,
|
||||
placeholder: strings.SearchQueryPlaceHolderText,
|
||||
onGetErrorMessage: this._validateEmptyField.bind(this)
|
||||
}),
|
||||
PropertyPaneTextField('queryTemplate', {
|
||||
label: strings.QueryTemplateFieldLabel,
|
||||
value: "{searchTerms} Path:{Site}",
|
||||
multiline: true,
|
||||
resizable: true,
|
||||
placeholder: strings.SearchQueryPlaceHolderText,
|
||||
|
|
|
@ -3,7 +3,8 @@ import IFilterPanelProps from "./IFilterPanelProps";
|
|||
import IFilterPanelState from "./IFilterPanelState";
|
||||
import { PrimaryButton, DefaultButton, IButtonProps } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
||||
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
|
||||
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||
import * as strings from "SearchWebPartStrings";
|
||||
import { IRefinementResult, IRefinementValue, IRefinementFilter } from "../../../models/ISearchResult";
|
||||
import { Link } from 'office-ui-fabric-react/lib/Link';
|
||||
|
@ -20,15 +21,9 @@ import { Scrollbars } from 'react-custom-scrollbars';
|
|||
|
||||
export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> {
|
||||
|
||||
private _initialFilters: IRefinementResult[];
|
||||
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
// The initialFilters are just set once and never updated afterwards so we don't need to put them in the component state.
|
||||
// We dont' want the refiners update every time to be able to revert changes easily in the interface and don't lose initial refiners.
|
||||
this._initialFilters = this.props.availableFilters;
|
||||
|
||||
this.state = {
|
||||
showPanel: false,
|
||||
selectedFilters: [],
|
||||
|
@ -52,7 +47,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
let groups: IGroup[] = [];
|
||||
|
||||
// Initialize the Office UI grouped list
|
||||
this._initialFilters.map((filter, i) => {
|
||||
this.props.availableFilters.map((filter, i) => {
|
||||
|
||||
groups.push({
|
||||
key: i.toString(),
|
||||
|
@ -76,12 +71,12 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
};
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
<Checkbox
|
||||
key={ j }
|
||||
checked= { this._isInFilterSelection(currentRefinement) }
|
||||
disabled={ false }
|
||||
label={ Text.format(refinementValue.RefinementValue + " ({0})", refinementValue.RefinementCount)}
|
||||
onChanged= {(checked: boolean) => {
|
||||
label={ Text.format(refinementValue.RefinementValue + " ({0})", refinementValue.RefinementCount)}
|
||||
onChange= {(ev, checked: boolean) => {
|
||||
// Every time we chek/uncheck a filter, a complete new search request is performed with current selected refiners
|
||||
checked ? this._addFilter(currentRefinement): this._removeFilter(currentRefinement);
|
||||
}} />
|
||||
|
@ -96,13 +91,10 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
const renderSelectedFilters: JSX.Element[] = this.state.selectedFilters.map((filter) => {
|
||||
|
||||
return (
|
||||
<PrimaryButton
|
||||
key ={filter.Value.RefinementToken }
|
||||
className="searchWp__selectedFilters__filterBtn"
|
||||
iconProps={ { iconName: 'StatusErrorFull' } }
|
||||
text={ filter.Value.RefinementName }
|
||||
onClick={ ()=> { this._removeFilter(filter); }}
|
||||
/>
|
||||
<Label className="filter">
|
||||
<i className="ms-Icon ms-Icon--ClearFilter" onClick={ ()=> { this._removeFilter(filter); }}></i>
|
||||
{ filter.Value.RefinementName }
|
||||
</Label>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -129,7 +121,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
{ (this.state.selectedFilters.length > 0) ?
|
||||
|
||||
<div className="searchWp__selectedFilters">
|
||||
<Label className="searchWp__selectedFilterLbl ms-font-s">{ strings.SelectedFiltersLabel }</Label>
|
||||
{ renderSelectedFilters }
|
||||
</div>
|
||||
: null
|
||||
|
@ -137,16 +128,17 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
<Panel
|
||||
className="filterPanel"
|
||||
isOpen={ this.state.showPanel }
|
||||
type={ PanelType.smallFixedFar }
|
||||
type={ PanelType.smallFixedNear }
|
||||
isBlocking={ false }
|
||||
isLightDismiss= { true }
|
||||
onDismiss={ this._onClosePanel }
|
||||
headerText={ strings.FilterPanelTitle }
|
||||
closeButtonAriaLabel='Close'
|
||||
hasCloseButton={ true }
|
||||
headerClassName="filterPanel__header"
|
||||
headerClassName="filterPanel__header"
|
||||
|
||||
onRenderBody={() => {
|
||||
if(this._initialFilters.length > 0) {
|
||||
if (this.props.availableFilters.length > 0) {
|
||||
return (
|
||||
<Scrollbars style={{ height: "100%" }}>
|
||||
<div className="filterPanel__body">
|
||||
|
@ -245,7 +237,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
|
||||
let allFilters: IRefinementFilter[] = [];
|
||||
|
||||
this._initialFilters.map((filter) => {
|
||||
this.props.availableFilters.map((filter) => {
|
||||
|
||||
filter.Values.map((refinementValue: IRefinementValue, index) => {
|
||||
allFilters.push({FilterName: filter.FilterName, Value: refinementValue});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import ISearchDataProvider from "../../../dataProviders/ISearchDataProvider";
|
||||
|
||||
interface ISearchContainerProps {
|
||||
dataProvider: ISearchDataProvider;
|
||||
searchQuery: string;
|
||||
searchDataProvider: ISearchDataProvider;
|
||||
queryKeywords: string;
|
||||
maxResultsCount: number;
|
||||
selectedProperties: string[];
|
||||
refiners: string;
|
||||
|
|
|
@ -1,12 +1,46 @@
|
|||
import { ISearchResults, IRefinementFilter } from "../../../models/ISearchResult";
|
||||
import { ISearchResults, IRefinementFilter, IRefinementResult } from "../../../models/ISearchResult";
|
||||
|
||||
interface ISearchContainerState {
|
||||
results?: ISearchResults;
|
||||
selectedFilters?: IRefinementFilter[];
|
||||
currentPage?: number;
|
||||
errorMessage?: string;
|
||||
hasError?: boolean;
|
||||
areResultsLoading?: boolean;
|
||||
|
||||
/**
|
||||
* The current search results to display
|
||||
*/
|
||||
results: ISearchResults;
|
||||
|
||||
/**
|
||||
* Current selected filters to apply to the search query. We need this information during page transition to keep existing filters
|
||||
*/
|
||||
selectedFilters: IRefinementFilter[];
|
||||
|
||||
/**
|
||||
* Available filters in the filter panel
|
||||
*/
|
||||
availableFilters: IRefinementResult[];
|
||||
|
||||
/**
|
||||
* The current result page number
|
||||
*/
|
||||
currentPage: number;
|
||||
|
||||
/**
|
||||
* Error message display in the message bar
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
/**
|
||||
* Indicates whether or not there is an error in the component
|
||||
*/
|
||||
hasError: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the resutls arre currenty loading due to a refinement or new query
|
||||
*/
|
||||
areResultsLoading: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the componetn loads for the first time
|
||||
*/
|
||||
isComponentLoading: boolean;
|
||||
}
|
||||
|
||||
export default ISearchContainerState;
|
|
@ -10,6 +10,7 @@ import TilesList from "../TilesList/TilesList";
|
|||
import "../SearchWebPart.scss";
|
||||
import FilterPanel from "../FilterPanel/FilterPanel";
|
||||
import Paging from "../Paging/Paging";
|
||||
import { Overlay } from "office-ui-fabric-react";
|
||||
|
||||
export default class SearchContainer extends React.Component<ISearchContainerProps,ISearchContainerState> {
|
||||
|
||||
|
@ -24,8 +25,10 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
|
|||
RelevantResults: []
|
||||
},
|
||||
selectedFilters: [],
|
||||
availableFilters: [],
|
||||
currentPage: 1,
|
||||
areResultsLoading: true,
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: true,
|
||||
errorMessage: "",
|
||||
hasError: false,
|
||||
};
|
||||
|
@ -40,33 +43,38 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
|
|||
const items = this.state.results;
|
||||
const hasError = this.state.hasError;
|
||||
const errorMessage = this.state.errorMessage;
|
||||
const isComponentLoading = this.state.isComponentLoading;
|
||||
|
||||
let wpContent: JSX.Element = null;
|
||||
let renderOverlay = null;
|
||||
|
||||
if (!isComponentLoading && areResultsLoading) {
|
||||
renderOverlay = <div>
|
||||
<Overlay isDarkThemed={false} className="overlay">
|
||||
</Overlay>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (areResultsLoading) {
|
||||
if (isComponentLoading) {
|
||||
wpContent = <Spinner size={ SpinnerSize.large } label={ strings.LoadingMessage } />;
|
||||
} else {
|
||||
|
||||
if (hasError) {
|
||||
|
||||
wpContent = <MessageBar messageBarType= { MessageBarType.error }>{ errorMessage }</MessageBar>;
|
||||
|
||||
} else {
|
||||
|
||||
if (items.RelevantResults.length === 0) {
|
||||
|
||||
wpContent =
|
||||
<div>
|
||||
<FilterPanel availableFilters={ items.RefinementResults } onUpdateFilters={ this._onUpdateFilters }/>
|
||||
<FilterPanel availableFilters={ this.state.availableFilters } onUpdateFilters={ this._onUpdateFilters }/>
|
||||
<div className="searchWp__noresult">{ strings.NoResultMessage }</div>
|
||||
</div>;
|
||||
|
||||
} else {
|
||||
|
||||
wpContent =
|
||||
|
||||
<div>
|
||||
<FilterPanel availableFilters={ items.RefinementResults } onUpdateFilters={ this._onUpdateFilters }/>
|
||||
<FilterPanel availableFilters={ this.state.availableFilters } onUpdateFilters={ this._onUpdateFilters }/>
|
||||
{ renderOverlay }
|
||||
<TilesList items={ items.RelevantResults }/>
|
||||
{ this.props.showPaging ?
|
||||
<Paging
|
||||
|
@ -88,55 +96,96 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
|
|||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
public async componentDidMount() {
|
||||
|
||||
// Async calls
|
||||
this._getSearchResults(this.props.searchQuery, this.props.refiners, this.state.selectedFilters, this.state.currentPage);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: ISearchContainerProps): void {
|
||||
|
||||
// Intermediate state to display the spinner before an async query
|
||||
this.setState({
|
||||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
// We reset the page number and refinement filters
|
||||
this._getSearchResults(nextProps.searchQuery, nextProps.refiners, [], 1);
|
||||
}
|
||||
|
||||
private _getSearchResults(searchQuery: string, refiners: string, refinementFilters?: IRefinementFilter[], pageNumber?: number) {
|
||||
|
||||
this.props.dataProvider.search(searchQuery, refiners, refinementFilters, pageNumber).then((searchResults: ISearchResults) => {
|
||||
try {
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
areResultsLoading: false,
|
||||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
}).catch((error) => {
|
||||
Logger.write("[SearchContainer._getSearchResults()]: Error: " + error, LogLevel.Error);
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, this.props.refiners, 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,
|
||||
// previous values will overwritten preventing to select multiple values (default SharePoint behavior)
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
availableFilters: searchResults.RefinementResults,
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
Logger.write("[SearchContainer._componentDidMount()]: Error: " + error, LogLevel.Error);
|
||||
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
results: { RefinementResults: [], RelevantResults: [] },
|
||||
hasError: true,
|
||||
errorMessage: error.message,
|
||||
errorMessage: error.message
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async componentWillReceiveProps(nextProps: ISearchContainerProps) {
|
||||
|
||||
// New props are passed to the component when the search query has been changed
|
||||
if (this.props.refiners.toString() !== nextProps.refiners.toString()
|
||||
|| this.props.maxResultsCount !== nextProps.maxResultsCount) {
|
||||
|
||||
try {
|
||||
|
||||
// Clear selected filters on a new query or new refiners
|
||||
this.setState({
|
||||
selectedFilters: [],
|
||||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
// We reset the page number and refinement filters
|
||||
const searchResults = await this.props.searchDataProvider.search(nextProps.queryKeywords, nextProps.refiners, [], 1);
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
availableFilters: searchResults.RefinementResults,
|
||||
areResultsLoading: false,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
Logger.write("[SearchContainer._componentWillReceiveProps()]: Error: " + error, LogLevel.Error);
|
||||
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
results: { RefinementResults: [], RelevantResults: [] },
|
||||
hasError: true,
|
||||
errorMessage: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function to apply new filters coming from the filter panel child component
|
||||
* @param newFilters The new filters to apply
|
||||
*/
|
||||
private _onUpdateFilters(newFilters: IRefinementFilter[]) {
|
||||
|
||||
this._getSearchResults(this.props.searchQuery, this.props.refiners, newFilters, 1);
|
||||
private async _onUpdateFilters(newFilters: IRefinementFilter[]) {
|
||||
|
||||
// Get back to the first page when new filters have been selected
|
||||
this.setState({
|
||||
selectedFilters: newFilters,
|
||||
currentPage: 1,
|
||||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, this.props.refiners, newFilters, 1);
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
areResultsLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -144,11 +193,18 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
|
|||
* Callback function update search results according the page number
|
||||
* @param pageNumber The page mumber to get
|
||||
*/
|
||||
private _onPageUpdate(pageNumber: number) {
|
||||
this._getSearchResults(this.props.searchQuery, this.props.refiners, this.state.selectedFilters, pageNumber);
|
||||
|
||||
private async _onPageUpdate(pageNumber: number) {
|
||||
|
||||
this.setState({
|
||||
currentPage: pageNumber,
|
||||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, this.props.refiners, this.state.selectedFilters, pageNumber);
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
areResultsLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
bottom: 0;
|
||||
padding: 8px 16px;
|
||||
color: #ffffff;
|
||||
background-color: #0078d7;
|
||||
background-color: "[theme: themePrimary]";
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
|
@ -55,16 +55,33 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
&__filterResultBtn, &__selectedFilterLbl {
|
||||
&__filterResultBtn {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
&__selectedFilters {
|
||||
|
||||
&__filterBtn {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 10px;
|
||||
margin-left: 10px;
|
||||
padding: 10px;
|
||||
|
||||
label.filter {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
label.filter~label.filter:before {
|
||||
padding: 5px;
|
||||
content: "|";
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
color: "[theme: themePrimary]";
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,12 +104,14 @@
|
|||
|
||||
a {
|
||||
float: left;
|
||||
color: "[theme: themePrimary]";
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 15px;
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
color: "[theme: themePrimary]";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,7 +120,7 @@
|
|||
}
|
||||
|
||||
a.active {
|
||||
background-color: #0078d7;
|
||||
background-color: "[theme: themePrimary]";
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +160,17 @@
|
|||
.ms-List-page~.ms-List-page {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"SearchSettingsGroupName": "Search settings",
|
||||
"SearchQueryFieldLabel": "Search query",
|
||||
"SearchQueryKeywordsFieldLabel": "Search query keywords",
|
||||
"QueryTemplateFieldLabel": "Query template",
|
||||
"SelectedPropertiesFieldLabel": "Selected Properties",
|
||||
"LoadingMessage": "Results are loading, please wait...",
|
||||
"MaxResultsCount": "Number of items to retrieve per page",
|
||||
"NoResultMessage": "There is no items to show",
|
||||
"NoResultMessage": "There is no results to show",
|
||||
"RefinersFieldLabel": "Refiners",
|
||||
"FilterPanelTitle": "Available filters",
|
||||
"FilterResultsButtonLabel": "Filter results",
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"SearchSettingsGroupName": "Paramètres de recherche",
|
||||
"SearchQueryFieldLabel": "Requête de recherche",
|
||||
"SearchQueryKeywordsFieldLabel": "Mots clés de recherche",
|
||||
"QueryTemplateFieldLabel": "Modèle de reqûete",
|
||||
"SelectedPropertiesFieldLabel": "Propriétés à récupérer",
|
||||
"LoadingMessage": "Les résultats sont en cours de chargement, veuillez patienter...",
|
||||
"MaxResultsCount": "Nombre de résulats à récupérer par page",
|
||||
"NoResultMessage": "Il n'y a aucun élément à afficher.",
|
||||
"NoResultMessage": "Il n'y a aucun résultat à afficher.",
|
||||
"RefinersFieldLabel": "Filtres",
|
||||
"FilterPanelTitle": "Filtres disponibles",
|
||||
"FilterResultsButtonLabel": "Filtrer l'affichage",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
declare interface ISearchWebPartStrings {
|
||||
SearchSettingsGroupName: string;
|
||||
SearchQueryFieldLabel: string;
|
||||
SearchQueryKeywordsFieldLabel: string;
|
||||
QueryTemplateFieldLabel: string;
|
||||
SelectedPropertiesFieldLabel: string;
|
||||
LoadingMessage: string;
|
||||
MaxResultsCount: string;
|
||||
|
|
Loading…
Reference in New Issue