Add sort panel (#656)
* Add SortPanel to select a field to sort by and it's sortorder * Fix bug with ordering the sorted results * apply state from searchresultcontainer to the sort panel * Fix issue with sortorder being undefined in certain scenarios * - Fix error on opening the web part without any settings configured - Show relevant error message when using an invalid sort field - Fix styling difference between 'Filter' and 'Sort' buttons * Add info to readme.md * Add new property_pane images to reflect latest changes * Remove black line at the bottom of property_pane3.png * Merge 'Dev' with current branch and show Sort button when "SortError" is shown * apply state from searchresultcontainer to the sort panel * - Fix error on opening the web part without any settings configured - Show relevant error message when using an invalid sort field - Fix styling difference between 'Filter' and 'Sort' buttons * Add info to readme.md * Add new property_pane images to reflect latest changes * Remove black line at the bottom of property_pane3.png * Add missing localization for SortPanel, Update componentReceiveProps, rename sortOrder to sortDirection, fix typo when setting sort direction
This commit is contained in:
parent
27977d9235
commit
2df0641eb7
|
@ -84,6 +84,7 @@ Result Source Identifier | The GUID of a SharePoint result source. If you specif
|
||||||
Enable Query Rules | Enable the query rules if applies
|
Enable Query Rules | Enable the query rules if applies
|
||||||
Selected properties | The search managed properties to retrieve. You can use these properties then in the code like this (`item.property_name`).
|
Selected properties | The search managed properties to retrieve. You can use these properties then in the code like this (`item.property_name`).
|
||||||
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",...
|
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",...
|
||||||
|
Sortable fields | The search managed properties to use for sorting. Make sure these are sortable. 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 sort panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",... If no sortable fields are provided, the 'Sort' button will not be visible.
|
||||||
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.
|
||||||
|
|
||||||
#### Styling Options ####
|
#### Styling Options ####
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 43 KiB |
Binary file not shown.
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 101 KiB |
Binary file not shown.
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 73 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
enum SortDirection {
|
||||||
|
Ascending = "ascending",
|
||||||
|
Descending = "descending"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SortDirection;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import SortDirection from "./SortDirection";
|
||||||
|
|
||||||
|
type UpdateSortOperationCallback = (sortDirection:SortDirection,sortField?:string) => void;
|
||||||
|
|
||||||
|
export default UpdateSortOperationCallback;
|
|
@ -103,8 +103,8 @@ class SearchService implements ISearchService {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (this._sortList) {
|
if (this._sortList) {
|
||||||
let sortOrders = this._sortList.split(',');
|
let sortDirections = this._sortList.split(',');
|
||||||
sortList = sortOrders.map(sorter => {
|
sortList = sortDirections.map(sorter => {
|
||||||
let sort = sorter.split(':');
|
let sort = sorter.split(':');
|
||||||
let s: Sort = { Property: sort[0].trim(), Direction: SortDirection.Descending };
|
let s: Sort = { Property: sort[0].trim(), Direction: SortDirection.Descending };
|
||||||
if (sort.indexOf('[') !== -1) {
|
if (sort.indexOf('[') !== -1) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface ISearchResultsWebPartProps {
|
||||||
maxResultsCount: number;
|
maxResultsCount: number;
|
||||||
selectedProperties: string;
|
selectedProperties: string;
|
||||||
refiners: string;
|
refiners: string;
|
||||||
|
sortableFields: string;
|
||||||
showPaging: boolean;
|
showPaging: boolean;
|
||||||
showResultsCount: boolean;
|
showResultsCount: boolean;
|
||||||
showBlank: boolean;
|
showBlank: boolean;
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._parseRefiners = this._parseRefiners.bind(this);
|
this._parseFieldListString = this._parseFieldListString.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,6 +145,14 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
value: this.properties.sortList,
|
value: this.properties.sortList,
|
||||||
deferredValidationTime: 300
|
deferredValidationTime: 300
|
||||||
}),
|
}),
|
||||||
|
PropertyPaneTextField('sortableFields', {
|
||||||
|
label: strings.SortableFieldsLabel,
|
||||||
|
description: strings.SortableFieldsDescription,
|
||||||
|
multiline: true,
|
||||||
|
resizable: true,
|
||||||
|
value: this.properties.sortableFields,
|
||||||
|
deferredValidationTime: 300,
|
||||||
|
}),
|
||||||
PropertyPaneToggle('enableQueryRules', {
|
PropertyPaneToggle('enableQueryRules', {
|
||||||
label: strings.EnableQueryRulesLabel,
|
label: strings.EnableQueryRulesLabel,
|
||||||
checked: this.properties.enableQueryRules,
|
checked: this.properties.enableQueryRules,
|
||||||
|
@ -388,12 +396,13 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses refiners from the property pane value by extracting the refiner managed property and its label in the filter panel.
|
* Parses a list of Fields from the property pane value by extracting the managed property and its label.
|
||||||
* @param rawValue the raw value of the refiner
|
* @param rawValue the raw value of the refiner
|
||||||
*/
|
*/
|
||||||
private _parseRefiners(rawValue: string): { [key: string]: string } {
|
private _parseFieldListString(rawValue: string): { [key: string]: string } {
|
||||||
|
|
||||||
let refiners = {};
|
let returnValues = {};
|
||||||
|
if(!rawValue) { return returnValues; }
|
||||||
|
|
||||||
// Get each configuration
|
// Get each configuration
|
||||||
let refinerKeyValuePair = rawValue.split(',');
|
let refinerKeyValuePair = rawValue.split(',');
|
||||||
|
@ -405,18 +414,18 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
switch (refinerValues.length) {
|
switch (refinerValues.length) {
|
||||||
case 1:
|
case 1:
|
||||||
// Take the same name as the refiner managed property
|
// Take the same name as the refiner managed property
|
||||||
refiners[refinerValues[0]] = refinerValues[0];
|
returnValues[refinerValues[0]] = refinerValues[0];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
// Trim quotes if present
|
// Trim quotes if present
|
||||||
refiners[refinerValues[0]] = refinerValues[1].replace(/^'(.*)'$/, '$1');
|
returnValues[refinerValues[0]] = refinerValues[1].replace(/^'(.*)'$/, '$1');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return refiners;
|
return returnValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -574,7 +583,8 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
sortList: this.properties.sortList,
|
sortList: this.properties.sortList,
|
||||||
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._parseRefiners(this.properties.refiners),
|
refiners: this._parseFieldListString(this.properties.refiners),
|
||||||
|
sortableFields: this._parseFieldListString(this.properties.sortableFields),
|
||||||
showPaging: this.properties.showPaging,
|
showPaging: this.properties.showPaging,
|
||||||
showResultsCount: this.properties.showResultsCount,
|
showResultsCount: this.properties.showResultsCount,
|
||||||
showBlank: this.properties.showBlank,
|
showBlank: this.properties.showBlank,
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="ms-textAlignRight">
|
<div className={`${styles.searchWp__buttonBar__button} ms-textAlignRight`}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className={`${styles.searchWp__filterResultBtn} ms-fontWeight-semibold`}
|
className={`${styles.searchWp__filterResultBtn} ms-fontWeight-semibold`}
|
||||||
iconProps={{ iconName: 'Filter' }}
|
iconProps={{ iconName: 'Filter' }}
|
||||||
|
@ -130,7 +130,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
<Panel
|
<Panel
|
||||||
className={styles.searchWp__filterPanel}
|
|
||||||
isOpen={this.state.showPanel}
|
isOpen={this.state.showPanel}
|
||||||
type={PanelType.custom}
|
type={PanelType.custom}
|
||||||
customWidth="450px"
|
customWidth="450px"
|
||||||
|
|
|
@ -56,6 +56,11 @@ interface ISearchResultsContainerProps {
|
||||||
*/
|
*/
|
||||||
refiners: { [key: string]: string };
|
refiners: { [key: string]: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The managed properties used as sortable fields for the query
|
||||||
|
*/
|
||||||
|
sortableFields: { [key: string]: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the paging control
|
* Show the paging control
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ISearchResults, IRefinementFilter, IRefinementResult } from '../../../../models/ISearchResult';
|
import { ISearchResults, IRefinementFilter, IRefinementResult } from '../../../../models/ISearchResult';
|
||||||
|
import SortDirection from '../../../../models/SortDirection';
|
||||||
|
|
||||||
interface ISearchResultsContainerState {
|
interface ISearchResultsContainerState {
|
||||||
|
|
||||||
|
@ -46,6 +47,16 @@ interface ISearchResultsContainerState {
|
||||||
* Keeps the last query in case you change the query in the propery panel
|
* Keeps the last query in case you change the query in the propery panel
|
||||||
*/
|
*/
|
||||||
lastQuery: string;
|
lastQuery: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the field on which the results need to be sorted (after initial sort)
|
||||||
|
*/
|
||||||
|
sortField?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the order in which the results need to be sorted (after initial sort)
|
||||||
|
*/
|
||||||
|
sortDirection?: SortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ISearchResultsContainerState;
|
export default ISearchResultsContainerState;
|
|
@ -13,6 +13,8 @@ import { DisplayMode } from '@microsoft/sp-core-library';
|
||||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||||
import SearchResultsTemplate from '../Layouts/SearchResultsTemplate';
|
import SearchResultsTemplate from '../Layouts/SearchResultsTemplate';
|
||||||
import styles from '../SearchResultsWebPart.module.scss';
|
import styles from '../SearchResultsWebPart.module.scss';
|
||||||
|
import { SortPanel } from '../SortPanel';
|
||||||
|
import SortDirection from '../../../../models/SortDirection';
|
||||||
|
|
||||||
declare var System: any;
|
declare var System: any;
|
||||||
let FilterPanel = null;
|
let FilterPanel = null;
|
||||||
|
@ -35,10 +37,11 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
areResultsLoading: false,
|
areResultsLoading: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
hasError: false,
|
hasError: false,
|
||||||
lastQuery: ''
|
lastQuery: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this._onUpdateFilters = this._onUpdateFilters.bind(this);
|
this._onUpdateFilters = this._onUpdateFilters.bind(this);
|
||||||
|
this._onUpdateSort = this._onUpdateSort.bind(this);
|
||||||
this._onPageUpdate = this._onPageUpdate.bind(this);
|
this._onPageUpdate = this._onPageUpdate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,8 +86,18 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
renderWebPartTitle = <WebPartTitle title={this.props.webPartTitle} updateProperty={null} displayMode={DisplayMode.Read} />;
|
renderWebPartTitle = <WebPartTitle title={this.props.webPartTitle} updateProperty={null} displayMode={DisplayMode.Read} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortPanel = <SortPanel
|
||||||
|
onUpdateSort={this._onUpdateSort}
|
||||||
|
sortableFieldsConfiguration={this.props.sortableFields}
|
||||||
|
sortDirection={this.state.sortDirection}
|
||||||
|
sortField={this.state.sortField} />;
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
renderWpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
|
if(this.state.errorMessage === strings.SortErrorMessage)
|
||||||
|
{
|
||||||
|
renderWpContent = <div><div className={styles.searchWp__buttonBar}>{sortPanel}</div><MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar></div>;
|
||||||
|
}else {
|
||||||
|
renderWpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
const currentQuery = this.props.queryKeywords + this.props.searchService.queryTemplate + this.props.selectedProperties.join(',');
|
const currentQuery = this.props.queryKeywords + this.props.searchService.queryTemplate + this.props.selectedProperties.join(',');
|
||||||
|
@ -94,7 +107,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
onUpdateFilters={this._onUpdateFilters}
|
onUpdateFilters={this._onUpdateFilters}
|
||||||
refinersConfiguration={this.props.refiners}
|
refinersConfiguration={this.props.refiners}
|
||||||
resetSelectedFilters={ this.state.lastQuery !== currentQuery ? true : false}
|
resetSelectedFilters={ this.state.lastQuery !== currentQuery ? true : false}
|
||||||
/> : <span />;
|
/> : <span />;
|
||||||
|
|
||||||
if (items.RelevantResults.length === 0) {
|
if (items.RelevantResults.length === 0) {
|
||||||
|
|
||||||
|
@ -103,7 +116,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
renderWpContent =
|
renderWpContent =
|
||||||
<div>
|
<div>
|
||||||
{renderWebPartTitle}
|
{renderWebPartTitle}
|
||||||
{renderFilterPanel}
|
<div className={styles.searchWp__buttonBar}>{sortPanel}{renderFilterPanel}</div>
|
||||||
<div className={styles.searchWp__noresult}>{strings.NoResultMessage}</div>
|
<div className={styles.searchWp__noresult}>{strings.NoResultMessage}</div>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,7 +129,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
renderWpContent =
|
renderWpContent =
|
||||||
<div>
|
<div>
|
||||||
{renderWebPartTitle}
|
{renderWebPartTitle}
|
||||||
{renderFilterPanel}
|
<div className={styles.searchWp__buttonBar}>{sortPanel}{renderFilterPanel}</div>
|
||||||
{renderOverlay}
|
{renderOverlay}
|
||||||
<SearchResultsTemplate
|
<SearchResultsTemplate
|
||||||
templateService={this.props.templateService}
|
templateService={this.props.templateService}
|
||||||
|
@ -157,7 +170,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
|
|
||||||
// Don't perform search is there is no keywords
|
// Don't perform search if there are no keywords
|
||||||
if (this.props.queryKeywords) {
|
if (this.props.queryKeywords) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
@ -212,6 +225,8 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
|
|
||||||
// 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 (JSON.stringify(this.props.refiners) !== JSON.stringify(nextProps.refiners)
|
if (JSON.stringify(this.props.refiners) !== JSON.stringify(nextProps.refiners)
|
||||||
|
|| JSON.stringify(this.props.sortableFields) !== JSON.stringify(nextProps.sortableFields)
|
||||||
|
|| this.props.sortList !== nextProps.sortList
|
||||||
|| this.props.maxResultsCount !== nextProps.maxResultsCount
|
|| this.props.maxResultsCount !== nextProps.maxResultsCount
|
||||||
|| this.state.lastQuery !== query
|
|| this.state.lastQuery !== query
|
||||||
|| this.props.resultSourceId !== nextProps.resultSourceId
|
|| this.props.resultSourceId !== nextProps.resultSourceId
|
||||||
|
@ -225,12 +240,17 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedFilters: [],
|
selectedFilters: [],
|
||||||
areResultsLoading: true,
|
areResultsLoading: true,
|
||||||
|
hasError: false,
|
||||||
|
errorMessage: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.searchService.selectedProperties = nextProps.selectedProperties;
|
this.props.searchService.selectedProperties = nextProps.selectedProperties;
|
||||||
|
|
||||||
const refinerManagedProperties = Object.keys(nextProps.refiners).join(',');
|
const refinerManagedProperties = Object.keys(nextProps.refiners).join(',');
|
||||||
|
|
||||||
|
// Reset sortlist
|
||||||
|
this.props.searchService.sortList = this.props.sortList;
|
||||||
|
|
||||||
// We reset the page number and refinement filters
|
// We reset the page number and refinement filters
|
||||||
const searchResults = await this.props.searchService.search(nextProps.queryKeywords, refinerManagedProperties, [], 1);
|
const searchResults = await this.props.searchService.search(nextProps.queryKeywords, refinerManagedProperties, [], 1);
|
||||||
const localizedFilters = await this._getLocalizedFilters(searchResults.RefinementResults);
|
const localizedFilters = await this._getLocalizedFilters(searchResults.RefinementResults);
|
||||||
|
@ -315,6 +335,48 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function to apply new sort configuration coming from the sort panel child component
|
||||||
|
* @param newFilters The new filters to apply
|
||||||
|
*/
|
||||||
|
private async _onUpdateSort(sortDirection:SortDirection,sortField?:string) {
|
||||||
|
if(sortField) {
|
||||||
|
// Get back to the first page when new sorting has been selected
|
||||||
|
this.setState({
|
||||||
|
sortField: sortField,
|
||||||
|
sortDirection: sortDirection,
|
||||||
|
currentPage: 1,
|
||||||
|
areResultsLoading: true,
|
||||||
|
hasError:false,
|
||||||
|
errorMessage:null
|
||||||
|
});
|
||||||
|
|
||||||
|
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
|
||||||
|
|
||||||
|
this.props.searchService.sortList = `${sortField}:${sortDirection}`;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const searchResults = await this.props.searchService.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, 1);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
results: searchResults,
|
||||||
|
areResultsLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
Logger.write('[SearchContainer._onUpdateSort(sortDirection:SortDirection,sortField?:string)]: Error: ' + error, LogLevel.Error);
|
||||||
|
const errorMessage = /\"value\":\"[^:]+: SortList\.\"/.test(error.message) ? strings.SortErrorMessage : error.message;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
areResultsLoading: false,
|
||||||
|
results: { RefinementResults: [], RelevantResults: [] },
|
||||||
|
hasError: true,
|
||||||
|
errorMessage: errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback function update search results according the page number
|
* Callback function update search results according the page number
|
||||||
* @param pageNumber The page mumber to get
|
* @param pageNumber The page mumber to get
|
||||||
|
|
|
@ -10,10 +10,26 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__buttonBar {
|
||||||
|
overflow:auto;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__filterResultBtn {
|
&__filterResultBtn {
|
||||||
color: "[theme: themePrimary, default: #005a9e]";
|
color: "[theme: themePrimary, default: #005a9e]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__sortResultBtn {
|
||||||
|
color: "[theme: themePrimary, default: #005a9e]";
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sortResultBtn {
|
||||||
|
color: "[theme: themePrimary]";
|
||||||
|
}
|
||||||
|
|
||||||
&__selectedFilters {
|
&__selectedFilters {
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -83,9 +99,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__filterPanel {
|
&__filterPanel {
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
@ -124,6 +137,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__sortPanel {
|
||||||
|
&__body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import UpdateSortOperationCallback from '../../../../models/UpdateSortOperationCallback';
|
||||||
|
import SortDirection from '../../../../models/SortDirection';
|
||||||
|
|
||||||
|
interface ISortPanelProps {
|
||||||
|
sortableFieldsConfiguration: { [key: string]: string };
|
||||||
|
onUpdateSort: UpdateSortOperationCallback;
|
||||||
|
sortDirection?:SortDirection;
|
||||||
|
sortField?:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ISortPanelProps;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { IRefinementFilter } from '../../../../models/ISearchResult';
|
||||||
|
import SortDirection from '../../../../models/SortDirection';
|
||||||
|
|
||||||
|
interface IFilterPanelState {
|
||||||
|
showPanel?: boolean;
|
||||||
|
sortField?: string;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IFilterPanelState;
|
|
@ -0,0 +1,134 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import ISortPanelProps from './ISortPanelProps';
|
||||||
|
import ISortPanelState from './ISortPanelState';
|
||||||
|
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
||||||
|
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
|
||||||
|
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||||
|
import * as strings from 'SearchWebPartStrings';
|
||||||
|
import { Scrollbars } from 'react-custom-scrollbars';
|
||||||
|
import { ActionButton } from 'office-ui-fabric-react/lib/Button';
|
||||||
|
import SortDirection from '../../../../models/SortDirection';
|
||||||
|
import styles from '../SearchResultsWebPart.module.scss';
|
||||||
|
|
||||||
|
export default class SortPanel extends React.Component<ISortPanelProps, ISortPanelState> {
|
||||||
|
|
||||||
|
public constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showPanel: false,
|
||||||
|
sortDirection:this.props.sortDirection ? this.props.sortDirection :SortDirection.Ascending,
|
||||||
|
sortField:this.props.sortField ? this.props.sortField : null
|
||||||
|
};
|
||||||
|
|
||||||
|
this._onTogglePanel = this._onTogglePanel.bind(this);
|
||||||
|
this._onClosePanel = this._onClosePanel.bind(this);
|
||||||
|
this._getSortableFieldCount = this._getSortableFieldCount.bind(this);
|
||||||
|
this._setSortDirection = this._setSortDirection.bind(this);
|
||||||
|
this._getDropdownOptions = this._getDropdownOptions.bind(this);
|
||||||
|
this._onChangedSelectedField = this._onChangedSelectedField.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<ISortPanelProps> {
|
||||||
|
if(this._getSortableFieldCount() === 0) return <span />;
|
||||||
|
|
||||||
|
const dropdownOptions: IDropdownOption[] = this._getDropdownOptions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={`${styles.searchWp__buttonBar__button} ms-textAlignRight`}>
|
||||||
|
<ActionButton
|
||||||
|
className={`${styles.searchWp__filterResultBtn} ms-fontWeight-semibold`}
|
||||||
|
iconProps={{ iconName:'Sort' }}
|
||||||
|
text={strings.SortResultsButtonLabel}
|
||||||
|
onClick={this._onTogglePanel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Panel
|
||||||
|
isOpen={this.state.showPanel}
|
||||||
|
type={PanelType.custom}
|
||||||
|
customWidth="450px"
|
||||||
|
isBlocking={false}
|
||||||
|
isLightDismiss={true}
|
||||||
|
onDismiss={this._onClosePanel}
|
||||||
|
headerText={strings.SortPanelTitle}
|
||||||
|
closeButtonAriaLabel={strings.PanelCloseButtonAria}
|
||||||
|
hasCloseButton={true}
|
||||||
|
onRenderBody={() => {
|
||||||
|
return <Scrollbars style={{ height: '100%' }}>
|
||||||
|
<div className={styles.searchWp__sortPanel__body}>
|
||||||
|
<div>
|
||||||
|
<Dropdown
|
||||||
|
placeHolder={strings.SortPanelSortFieldPlaceHolder}
|
||||||
|
label={strings.SortPanelSortFieldLabel}
|
||||||
|
ariaLabel={strings.SortPanelSortFieldAria}
|
||||||
|
onChanged={this._onChangedSelectedField}
|
||||||
|
selectedKey={this.state.sortField}
|
||||||
|
options={dropdownOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
label={strings.SortPanelSortDirectionLabel}
|
||||||
|
onText={strings.SortDirectionAscendingLabel}
|
||||||
|
offText={strings.SortDirectionDescendingLabel}
|
||||||
|
onChanged={(checked: boolean) => {
|
||||||
|
this._setSortDirection(checked);
|
||||||
|
}}
|
||||||
|
checked={this.state.sortDirection === SortDirection.Ascending || typeof(this.state.sortDirection) === 'undefined'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Scrollbars>;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSortableFieldCount() {
|
||||||
|
if(!this.props.sortableFieldsConfiguration) return 0;
|
||||||
|
|
||||||
|
return Object.keys(this.props.sortableFieldsConfiguration).filter(value => {
|
||||||
|
return value;
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setSortDirection(checked:boolean) {
|
||||||
|
const sortDirection = checked ? SortDirection.Ascending : SortDirection.Descending;
|
||||||
|
this.setState({
|
||||||
|
sortDirection: sortDirection,
|
||||||
|
});
|
||||||
|
this.props.onUpdateSort(sortDirection,this.state.sortField);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getDropdownOptions():IDropdownOption[] {
|
||||||
|
let dropdownOptions:IDropdownOption[] = [];
|
||||||
|
const sortableFields = Object.keys(this.props.sortableFieldsConfiguration);
|
||||||
|
|
||||||
|
sortableFields.forEach((fieldKey) => {
|
||||||
|
//Strip " from start and end of the display name if present
|
||||||
|
const fieldDisplayName = this.props.sortableFieldsConfiguration[fieldKey].replace(/^\"+|\"+$/g, '');
|
||||||
|
dropdownOptions.push({ key: fieldKey, text: fieldDisplayName});
|
||||||
|
});
|
||||||
|
return dropdownOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onChangedSelectedField(option: IDropdownOption, index?: number):void {
|
||||||
|
const sortField = option.key.toString();
|
||||||
|
this.setState({
|
||||||
|
sortField: sortField,
|
||||||
|
});
|
||||||
|
this.props.onUpdateSort(this.state.sortDirection,sortField);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onClosePanel() {
|
||||||
|
this.setState({ showPanel: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onTogglePanel() {
|
||||||
|
this.setState({ showPanel: !this.state.showPanel });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as SortPanel } from './SortPanel';
|
|
@ -9,8 +9,11 @@ define([], function() {
|
||||||
"MaxResultsCount": "Number of items to retrieve per page",
|
"MaxResultsCount": "Number of items to retrieve per page",
|
||||||
"NoResultMessage": "There are no results to show",
|
"NoResultMessage": "There are no results to show",
|
||||||
"RefinersFieldLabel": "Refiners",
|
"RefinersFieldLabel": "Refiners",
|
||||||
|
"SortableFieldsLabel": "Sortable fields",
|
||||||
"FilterPanelTitle": "Available filters",
|
"FilterPanelTitle": "Available filters",
|
||||||
|
"SortPanelTitle":"Sort",
|
||||||
"FilterResultsButtonLabel": "Filters",
|
"FilterResultsButtonLabel": "Filters",
|
||||||
|
"SortResultsButtonLabel":"Sort",
|
||||||
"SelectedFiltersLabel": "Selected filters:",
|
"SelectedFiltersLabel": "Selected filters:",
|
||||||
"ApplyAllFiltersLabel": "Apply all filters",
|
"ApplyAllFiltersLabel": "Apply all filters",
|
||||||
"RemoveAllFiltersLabel": "Remove all filters",
|
"RemoveAllFiltersLabel": "Remove all filters",
|
||||||
|
@ -26,13 +29,14 @@ define([], function() {
|
||||||
"PlaceHolderIconText": "Search Results Web Part with Refinements",
|
"PlaceHolderIconText": "Search Results Web Part with Refinements",
|
||||||
"PlaceHolderDescription": "This component displays search results with paging and customizable refinement panel",
|
"PlaceHolderDescription": "This component displays search results with paging and customizable refinement panel",
|
||||||
"ResultSourceIdLabel": "Result Source Identifier",
|
"ResultSourceIdLabel": "Result Source Identifier",
|
||||||
"SortList": "Sort order",
|
"SortList": "Initial sort order",
|
||||||
"SortListDescription": "Specify sort order in a comma separated list on the format <Managed Property Name>:ascending/descending (default:Created:descending,Size:ascending).",
|
"SortListDescription": "Specify initial sort order in a comma separated list on the format <Managed Property Name>:ascending/descending (default:Created:descending,Size:ascending).",
|
||||||
"InvalidResultSourceIdMessage": "Invalid identifier",
|
"InvalidResultSourceIdMessage": "Invalid identifier",
|
||||||
"UseSearchBoxQueryLabel": "Use a dynamic data source as search query",
|
"UseSearchBoxQueryLabel": "Use a dynamic data source as search 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). You can specify the label by using the following format <Managed Property Name>:\"My friendly name\".",
|
"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\".",
|
||||||
|
"SortableFieldsDescription": "Specifies sortable properties used by the sort panel (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.",
|
||||||
"CountMessageLong": "<b>{0}</b> results for '<em>{1}</em>'",
|
"CountMessageLong": "<b>{0}</b> results for '<em>{1}</em>'",
|
||||||
|
@ -53,6 +57,14 @@ define([], function() {
|
||||||
"HandlebarsHelpersDescription": "Enable functions from moment and handlebars helpers. See https://github.com/SharePoint/sp-dev-fx-webparts/blob/master/samples/react-search-refiners/README.md#available-tokens for more information.",
|
"HandlebarsHelpersDescription": "Enable functions from moment and handlebars helpers. See https://github.com/SharePoint/sp-dev-fx-webparts/blob/master/samples/react-search-refiners/README.md#available-tokens for more information.",
|
||||||
"DynamicDataSourceLabel": "Available data sources",
|
"DynamicDataSourceLabel": "Available data sources",
|
||||||
"DynamicDataSourcePropertyLabel": "Available properties",
|
"DynamicDataSourcePropertyLabel": "Available properties",
|
||||||
"PromotedResultsLabel": "Promoted result(s)"
|
"PromotedResultsLabel": "Promoted result(s)",
|
||||||
|
"SortDirectionAscendingLabel":"Ascending",
|
||||||
|
"SortDirectionDescendingLabel":"Descending",
|
||||||
|
"SortErrorMessage":"Invalid search property (Check if the managed property is sortable).",
|
||||||
|
"SortPanelSortFieldLabel":"Sort on field",
|
||||||
|
"SortPanelSortFieldAria":"Select a field",
|
||||||
|
"SortPanelSortFieldPlaceHolder":"Select a field",
|
||||||
|
"SortPanelSortDirectionLabel":"Sort Direction",
|
||||||
|
"PanelCloseButtonAria":"Close",
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -9,8 +9,11 @@ define([], function() {
|
||||||
"MaxResultsCount": "Nombre de résulats à récupérer par page",
|
"MaxResultsCount": "Nombre de résulats à récupérer par page",
|
||||||
"NoResultMessage": "Il n'y a aucun résultat à afficher.",
|
"NoResultMessage": "Il n'y a aucun résultat à afficher.",
|
||||||
"RefinersFieldLabel": "Filtres",
|
"RefinersFieldLabel": "Filtres",
|
||||||
|
"SortableFieldsLabel": "Triables",
|
||||||
"FilterPanelTitle": "Filtres disponibles",
|
"FilterPanelTitle": "Filtres disponibles",
|
||||||
|
"SortPanelTitle":"Trier",
|
||||||
"FilterResultsButtonLabel": "Filtrer",
|
"FilterResultsButtonLabel": "Filtrer",
|
||||||
|
"SortResultsButtonLabel":"Trier",
|
||||||
"SelectedFiltersLabel": "Filtre(s) appliqué(s):",
|
"SelectedFiltersLabel": "Filtre(s) appliqué(s):",
|
||||||
"ApplyAllFiltersLabel": "Appliquer tous les filters",
|
"ApplyAllFiltersLabel": "Appliquer tous les filters",
|
||||||
"RemoveAllFiltersLabel": "Supprimer tous les filtres",
|
"RemoveAllFiltersLabel": "Supprimer tous les filtres",
|
||||||
|
@ -33,6 +36,7 @@ define([], function() {
|
||||||
"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). Vous pouvez spécifier un label personnalisé en utilisant le format suivant <Nom de la propriété gérée>:\"Nom convivial\".",
|
"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\".",
|
||||||
|
"SortableFieldsDescription": "Propriétés gerées à utiliser comme triables (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.",
|
||||||
"CountMessageLong": "<b>{0}</b> résultats pour '<em>{1}</em>'",
|
"CountMessageLong": "<b>{0}</b> résultats pour '<em>{1}</em>'",
|
||||||
|
@ -53,6 +57,14 @@ define([], function() {
|
||||||
"HandlebarsHelpersDescription": "Activer les fonctions de moment et handlebars helpers. Voir https://github.com/SharePoint/sp-dev-fx-webparts/blob/master/samples/react-search-refiners/README.md#available-tokens pour plus d'informations.",
|
"HandlebarsHelpersDescription": "Activer les fonctions de moment et handlebars helpers. Voir https://github.com/SharePoint/sp-dev-fx-webparts/blob/master/samples/react-search-refiners/README.md#available-tokens pour plus d'informations.",
|
||||||
"DynamicDataSourceLabel": "Source de données disponibles",
|
"DynamicDataSourceLabel": "Source de données disponibles",
|
||||||
"DynamicDataSourcePropertyLabel": "Propriétés disponibles",
|
"DynamicDataSourcePropertyLabel": "Propriétés disponibles",
|
||||||
"PromotedResultsLabel": "Résultat(s) promu(s)"
|
"PromotedResultsLabel": "Résultat(s) promu(s)",
|
||||||
|
"SortDirectionAscendingLabel":"Ascendant",
|
||||||
|
"SortDirectionDescendingLabel":"Descendant",
|
||||||
|
"SortErrorMessage":"Propriété de recherche non valide (Vérifiez si la propriété managée est triable).",
|
||||||
|
"SortPanelSortFieldLabel":"Trier sur le champ",
|
||||||
|
"SortPanelSortFieldAria":"Sélectionner un champ",
|
||||||
|
"SortPanelSortFieldPlaceHolder":"Sélectionner un champ",
|
||||||
|
"SortPanelSortDirectionLabel":"Direction de tri",
|
||||||
|
"PanelCloseButtonAria":"Proche",
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -10,9 +10,13 @@ declare interface ISearchWebPartStrings {
|
||||||
MaxResultsCount: string;
|
MaxResultsCount: string;
|
||||||
NoResultMessage: string;
|
NoResultMessage: string;
|
||||||
RefinersFieldLabel: string;
|
RefinersFieldLabel: string;
|
||||||
|
SortableFieldsLabel: string;
|
||||||
RefinersFieldDescription: string;
|
RefinersFieldDescription: string;
|
||||||
|
SortableFieldsDescription: string;
|
||||||
FilterPanelTitle: string;
|
FilterPanelTitle: string;
|
||||||
|
SortPanelTitle: string;
|
||||||
FilterResultsButtonLabel: string;
|
FilterResultsButtonLabel: string;
|
||||||
|
SortResultsButtonLabel:string;
|
||||||
SelectedFiltersLabel: string;
|
SelectedFiltersLabel: string;
|
||||||
ApplyAllFiltersLabel: string;
|
ApplyAllFiltersLabel: string;
|
||||||
RemoveAllFiltersLabel: string;
|
RemoveAllFiltersLabel: string;
|
||||||
|
@ -53,6 +57,14 @@ declare interface ISearchWebPartStrings {
|
||||||
DynamicDataSourceLabel: string;
|
DynamicDataSourceLabel: string;
|
||||||
DynamicDataSourcePropertyLabel: string;
|
DynamicDataSourcePropertyLabel: string;
|
||||||
PromotedResultsLabel: string;
|
PromotedResultsLabel: string;
|
||||||
|
SortDirectionAscendingLabel:string;
|
||||||
|
SortDirectionDescendingLabel:string;
|
||||||
|
SortErrorMessage:string;
|
||||||
|
SortPanelSortFieldLabel:string;
|
||||||
|
SortPanelSortFieldAria:string;
|
||||||
|
SortPanelSortFieldPlaceHolder:string;
|
||||||
|
SortPanelSortDirectionLabel:string;
|
||||||
|
PanelCloseButtonAria:string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'SearchWebPartStrings' {
|
declare module 'SearchWebPartStrings' {
|
||||||
|
|
Loading…
Reference in New Issue