[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:
Franck Cornu 2017-12-20 10:51:40 -05:00 committed by Vesa Juvonen
parent cce1ff319c
commit 14f6753b62
18 changed files with 264 additions and 120 deletions

View File

@ -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.

View File

@ -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

View File

@ -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"

View File

@ -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;

View File

@ -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;
}
}

View File

@ -1,5 +1,6 @@
export interface ISearchWebPartProps {
searchQuery: string;
queryKeywords: string;
queryTemplate: string;
maxResultsCount: number;
selectedProperties: string;
refiners: string;

View File

@ -20,7 +20,8 @@
"description": { "default": "Displays search results with customizable dynamic refiners" },
"officeFabricIconFontName": "Search",
"properties": {
"searchQuery": "",
"queryKeywords": "",
"queryTemplate": "",
"refiners": "",
"selectedProperties": "",
"maxResultsCount": 10

View File

@ -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,

View File

@ -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});

View File

@ -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;

View File

@ -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;

View File

@ -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,
});
}
}

View File

@ -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;
}

View File

@ -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",

View File

@ -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",

View File

@ -1,6 +1,7 @@
declare interface ISearchWebPartStrings {
SearchSettingsGroupName: string;
SearchQueryFieldLabel: string;
SearchQueryKeywordsFieldLabel: string;
QueryTemplateFieldLabel: string;
SelectedPropertiesFieldLabel: string;
LoadingMessage: string;
MaxResultsCount: string;