Updated to drop 1.4, fixed property pane issues, and added more settings (#393)

* Updated to SPFx Drop 1.4

* Store last query in order to update the results when changing the search properties.

* Bugfix: Make sure selected properties are passed along and updated on each search query.

* Support more properties for preview image depending on item type.

* Moved default values to web part config instead of hard coded in the web part.

* Added support to show/hide file icon per result

* Remove empty check for query template as it's not required, but optional.

* Don't render filter control if there are no refiners specified.

* Added support to show/hide the created date field.

* Added more default properties for default image preview

* Updated preview image based on new properties

* Updated version to 1.1
Updated readme with new version, author and drop
This commit is contained in:
Mikael Svenson 2018-01-02 16:34:14 +01:00 committed by Vesa Juvonen
parent 14f6753b62
commit cf1d8d40de
19 changed files with 396 additions and 333 deletions

View File

@ -10,7 +10,7 @@ This sample shows you how to build user friendly SharePoint search experiences u
An associated [blog post](http://thecollaborationcorner.com/2017/10/16/build-dynamic-sharepoint-search-experiences-with-refiners-and-paging-with-spfx-office-ui-fabric-and-pnp-js-library/) is available to give you more details about this sample implementation.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.3.4-green.svg)
![drop](https://img.shields.io/badge/drop-1.4.0-green.svg)
## Applies to
@ -21,13 +21,14 @@ An associated [blog post](http://thecollaborationcorner.com/2017/10/16/build-dyn
Solution|Author(s)
--------|---------
react-search-refiners | Franck Cornu (MVP Office Development at aequos) - Twitter @FranckCornu
react-search-refiners | Franck Cornu (MVP Office Development at aequos) - [@FranckCornu](http://www.twitter.com/FranckCornu)<br/>Mikael Svenson -[@mikaelsvenson](http://www.twitter.com/mikaelsvenson)
## Version history
Version|Date|Comments
-------|----|--------
1.0 | January 03, 2018 | Initial release
1.0 | October 17, 2017 | Initial release
1.1 | January 03, 2018 | Improvements and updating to SPFx drop 1.4
## 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.**

View File

@ -3,7 +3,8 @@
"solution": {
"name": "PnP - Search and Refiners Web Part",
"id": "890affef-33e0-4d72-bd72-36399e02143b",
"version": "1.0.0.0"
"version": "1.1.0.0",
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/pnp-react-search-refiners.sppkg"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -11,31 +11,31 @@
"test": "gulp test"
},
"dependencies": {
"@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",
"@microsoft/sp-core-library": "~1.4.0",
"@microsoft/sp-lodash-subset": "~1.4.0",
"@microsoft/sp-webpart-base": "~1.4.0",
"@pnp/spfx-controls-react": "^1.1.1",
"@pnp/spfx-property-controls": "^1.1.1",
"@types/react": "15.6.6",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/react-dom": "15.5.6",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"immutability-helper": "2.4.0",
"lodash-es": "4.17.4",
"moment": "2.18.1",
"office-ui-fabric-react": "4.40.2-hotfix.1",
"react": "15.4.2",
"react": "15.6.2",
"react-custom-scrollbars": "4.1.2",
"react-dom": "15.4.2",
"react-dom": "15.6.2",
"react-js-pagination": "3.0.0",
"sp-pnp-js": "3.0.3"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.3.4",
"@microsoft/sp-module-interfaces": "~1.3.4",
"@microsoft/sp-webpart-workbench": "~1.3.4",
"@microsoft/sp-build-web": "~1.4.0",
"@microsoft/sp-module-interfaces": "~1.4.0",
"@microsoft/sp-webpart-workbench": "~1.4.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"

View File

@ -5,4 +5,6 @@ export interface ISearchWebPartProps {
selectedProperties: string;
refiners: string;
showPaging: boolean;
showFileIcon: boolean;
showCreatedDate: boolean;
}

View File

@ -1,30 +1,37 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "42ad2740-3c60-49cf-971a-c44e33511b93",
"alias": "SearchWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "PnP" },
"title": { "default": "Search with Refiners" },
"description": { "default": "Displays search results with customizable dynamic refiners" },
"officeFabricIconFontName": "Search",
"properties": {
"queryKeywords": "",
"queryTemplate": "",
"refiners": "",
"selectedProperties": "",
"maxResultsCount": 10
}
}]
}
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "42ad2740-3c60-49cf-971a-c44e33511b93",
"alias": "SearchWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": {
"default": "PnP"
},
"title": {
"default": "Search with Refiners"
},
"description": {
"default": "Displays search results with customizable dynamic refiners"
},
"officeFabricIconFontName": "Search",
"properties": {
"queryKeywords": "",
"queryTemplate": "{searchTerms} Path:{Site}",
"refiners": "Created",
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL",
"maxResultsCount": 10,
"showFileIcon": true,
"showCreatedDate": true
}
}
]
}

View File

@ -2,11 +2,11 @@ import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
PropertyPaneSlider,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneToggle
BaseClientSideWebPart,
PropertyPaneSlider,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
import * as strings from 'SearchWebPartStrings';
@ -21,140 +21,149 @@ import { Placeholder, IPlaceholderProps } from "@pnp/spfx-controls-react/lib/Pla
export default class SearchWebPart extends BaseClientSideWebPart<ISearchWebPartProps> {
private _dataProvider: ISearchDataProvider;
private _dataProvider: ISearchDataProvider;
/**
* Override the base onInit() implementation to get the persisted properties to initialize data provider.
*/
protected onInit(): Promise<void> {
/**
* Override the base onInit() implementation to get the persisted properties to initialize data provider.
*/
protected onInit(): Promise<void> {
// Init the moment JS library locale globally
const currentLocale = this.context.pageContext.cultureInfo.currentCultureName;
moment.locale(currentLocale);
// Init the moment JS library locale globally
const currentLocale = this.context.pageContext.cultureInfo.currentCultureName;
moment.locale(currentLocale);
if (Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockSearchDataProvider();
} else {
this._dataProvider = new SearchDataProvider(this.context);
}
return super.onInit();
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
public render(): void {
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,
{
searchDataProvider: this._dataProvider,
queryKeywords: this.properties.queryKeywords,
maxResultsCount: this.properties.maxResultsCount,
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(
Placeholder,
{
iconName: strings.PlaceHolderEditLabel,
iconText: strings.PlaceHolderIconText,
description: strings.PlaceHolderDescription,
buttonLabel: strings.PlaceHolderConfigureBtnLabel,
onConfigure: this._setupWebPart.bind(this)
}
);
renderElement = this.properties.queryKeywords ? searchContainer : placeholder;
ReactDom.render(renderElement, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
groups: [
{
groupName: strings.SearchSettingsGroupName,
groupFields: [
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,
onGetErrorMessage: this._validateEmptyField.bind(this)
}),
PropertyPaneTextField('selectedProperties', {
label: strings.SelectedPropertiesFieldLabel,
multiline: true,
resizable: true,
value: "Title,Path,Created,Filename,ServerRedirectedPreviewURL",
}),
PropertyPaneTextField('refiners', {
label: strings.RefinersFieldLabel,
multiline: true,
resizable: true,
value: "Created"
}),
PropertyPaneSlider("maxResultsCount", {
label: strings.MaxResultsCount,
max: 50,
min: 1,
showValue: true,
step: 1,
value: 50,
}),
PropertyPaneToggle("showPaging", {
label: strings.ShowPagingLabel,
checked: false,
}),
]
}
]
if (Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockSearchDataProvider();
} else {
this._dataProvider = new SearchDataProvider(this.context);
}
]
};
}
/**
* Opens the Web Part property pane
*/
private _setupWebPart() {
this.context.propertyPane.open();
}
private _validateEmptyField(value: string): string {
if (!value) {
return strings.EmptyFieldErrorMessage;
return super.onInit();
}
return "";
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
public render(): void {
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,
{
searchDataProvider: this._dataProvider,
queryKeywords: this.properties.queryKeywords,
maxResultsCount: this.properties.maxResultsCount,
selectedProperties: this.properties.selectedProperties ? this.properties.selectedProperties.replace(/\s|,+$/g, '').split(",") : [],
refiners: this.properties.refiners,
showPaging: this.properties.showPaging,
showFileIcon: this.properties.showFileIcon,
showCreatedDate: this.properties.showCreatedDate
} as ISearchContainerProps
);
const placeholder: React.ReactElement<IPlaceholderProps> = React.createElement(
Placeholder,
{
iconName: strings.PlaceHolderEditLabel,
iconText: strings.PlaceHolderIconText,
description: strings.PlaceHolderDescription,
buttonLabel: strings.PlaceHolderConfigureBtnLabel,
onConfigure: this._setupWebPart.bind(this)
}
);
renderElement = this.properties.queryKeywords ? searchContainer : placeholder;
ReactDom.render(renderElement, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
groups: [
{
groupName: strings.SearchSettingsGroupName,
groupFields: [
PropertyPaneTextField('queryKeywords', {
label: strings.SearchQueryKeywordsFieldLabel,
value: this.properties.queryKeywords,
multiline: true,
resizable: true,
placeholder: strings.SearchQueryPlaceHolderText,
onGetErrorMessage: this._validateEmptyField.bind(this)
}),
PropertyPaneTextField('queryTemplate', {
label: strings.QueryTemplateFieldLabel,
value: this.properties.queryTemplate,
multiline: true,
resizable: true,
placeholder: strings.SearchQueryPlaceHolderText
}),
PropertyPaneTextField('selectedProperties', {
label: strings.SelectedPropertiesFieldLabel,
multiline: true,
resizable: true,
value: this.properties.selectedProperties,
}),
PropertyPaneTextField('refiners', {
label: strings.RefinersFieldLabel,
multiline: true,
resizable: true,
value: this.properties.refiners
}),
PropertyPaneSlider("maxResultsCount", {
label: strings.MaxResultsCount,
max: 50,
min: 1,
showValue: true,
step: 1,
value: 50,
}),
PropertyPaneToggle("showPaging", {
label: strings.ShowPagingLabel,
checked: this.properties.showPaging,
}),
PropertyPaneToggle("showFileIcon", {
label: strings.ShowFileIconLabel,
checked: this.properties.showFileIcon,
}),
PropertyPaneToggle("showCreatedDate", {
label: strings.ShowCreatedDateLabel,
checked: this.properties.showCreatedDate,
}),
]
}
]
}
]
};
}
/**
* Opens the Web Part property pane
*/
private _setupWebPart() {
this.context.propertyPane.open();
}
private _validateEmptyField(value: string): string {
if (!value) {
return strings.EmptyFieldErrorMessage;
}
return "";
}
}

View File

@ -4,7 +4,7 @@ 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 { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
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';
@ -16,11 +16,11 @@ import {
GroupedList,
IGroup,
IGroupDividerProps
} from 'office-ui-fabric-react/lib/components/GroupedList/index';
} from 'office-ui-fabric-react/lib/components/GroupedList/index';
import { Scrollbars } from 'react-custom-scrollbars';
export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> {
public constructor(props) {
super(props);
@ -46,6 +46,8 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
let items: JSX.Element[] = [];
let groups: IGroup[] = [];
if (this.props.availableFilters.length === 0) return <span />;
// Initialize the Office UI grouped list
this.props.availableFilters.map((filter, i) => {
@ -59,7 +61,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
});
items.push(
<div key= { i }>
<div key={i}>
<div className="filterPanel__filterProperty">
{
filter.Values.map((refinementValue: IRefinementValue, j) => {
@ -72,14 +74,14 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
return (
<Checkbox
key={ j }
checked= { this._isInFilterSelection(currentRefinement) }
disabled={ false }
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);
}} />
key={j}
checked={this._isInFilterSelection(currentRefinement)}
disabled={false}
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);
}} />
);
})
}
@ -91,79 +93,79 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
const renderSelectedFilters: JSX.Element[] = this.state.selectedFilters.map((filter) => {
return (
<Label className="filter">
<i className="ms-Icon ms-Icon--ClearFilter" onClick={ ()=> { this._removeFilter(filter); }}></i>
{ filter.Value.RefinementName }
</Label>
<Label className="filter">
<i className="ms-Icon ms-Icon--ClearFilter" onClick={() => { this._removeFilter(filter); }}></i>
{filter.Value.RefinementName}
</Label>
);
});
const renderAvailableFilters = <GroupedList
ref='groupedList'
items={ items }
onRenderCell={ this._onRenderCell }
className="filterPanel__body__group"
groupProps={
{
onRenderHeader: this._onRenderHeader,
}
}
groups={ groups }/>;
const renderAvailableFilters = <GroupedList
ref='groupedList'
items={items}
onRenderCell={this._onRenderCell}
className="filterPanel__body__group"
groupProps={
{
onRenderHeader: this._onRenderHeader,
}
}
groups={groups} />;
return (
<div>
<DefaultButton
className="searchWp__filterResultBtn"
iconProps={ { iconName: 'Filter' } }
text={ strings.FilterResultsButtonLabel }
onClick= { this._onTogglePanel }
/>
{ (this.state.selectedFilters.length > 0) ?
iconProps={{ iconName: 'Filter' }}
text={strings.FilterResultsButtonLabel}
onClick={this._onTogglePanel}
/>
{(this.state.selectedFilters.length > 0) ?
<div className="searchWp__selectedFilters">
{ renderSelectedFilters }
</div>
: null
<div className="searchWp__selectedFilters">
{renderSelectedFilters}
</div>
: null
}
<Panel
className="filterPanel"
isOpen={ this.state.showPanel }
type={ PanelType.smallFixedNear }
isBlocking={ false }
isLightDismiss= { true }
onDismiss={ this._onClosePanel }
headerText={ strings.FilterPanelTitle }
closeButtonAriaLabel='Close'
hasCloseButton={ true }
isOpen={this.state.showPanel}
type={PanelType.smallFixedNear}
isBlocking={false}
isLightDismiss={true}
onDismiss={this._onClosePanel}
headerText={strings.FilterPanelTitle}
closeButtonAriaLabel='Close'
hasCloseButton={true}
headerClassName="filterPanel__header"
onRenderBody={() => {
onRenderBody={() => {
if (this.props.availableFilters.length > 0) {
return (
<Scrollbars style={{ height: "100%" }}>
<div className="filterPanel__body">
<div className="filterPanel__body__allFiltersToggle">
<Toggle
onText={ strings.RemoveAllFiltersLabel }
offText={ strings.ApplyAllFiltersLabel }
onChanged= {(checked: boolean) => {
<Toggle
onText={strings.RemoveAllFiltersLabel}
offText={strings.ApplyAllFiltersLabel}
onChanged={(checked: boolean) => {
checked ? this._applyAllfilters() : this._removeAllFilters();
}}
checked= { this.state.selectedFilters.length === 0 ? false : true }
checked={this.state.selectedFilters.length === 0 ? false : true}
/>
</div>
{ renderAvailableFilters }
{renderAvailableFilters}
</div>
</Scrollbars>
);
} else {
return (
<div className="filterPanel__body">
{ strings.NoFilterConfiguredLabel }
{strings.NoFilterConfiguredLabel}
</div>
);
}
}}>
}}>
</Panel>
</div>
);
@ -171,38 +173,38 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
private _onRenderCell(nestingDepth: number, item: any, itemIndex: number) {
return (
<div className="ms-Grid-row" data-selection-index={ itemIndex }>
<div className="ms-Grid-row" data-selection-index={itemIndex}>
<div className="ms-Grid-col ms-u-sm10 ms-u-md10 ms-u-lg10 ms-smPush1 ms-mdPush1 ms-lgPush1">
{ item }
</div>
</div>
{item}
</div>
</div>
);
}
private _onRenderHeader(props: IGroupDividerProps): JSX.Element {
return (
<div className="ms-Grid-row" onClick={ () => {
// Update the index for expanded groups to be able to keep it open after a re-render
const updatedExpandedGroups =
props.group.isCollapsed ?
update(this.state.expandedGroups, {$push: [props.group.startIndex]}) :
update(this.state.expandedGroups, {$splice: [[this.state.expandedGroups.indexOf(props.group.startIndex), 1]]});
this.setState({
expandedGroups: updatedExpandedGroups,
});
<div className="ms-Grid-row" onClick={() => {
props.onToggleCollapse(props.group);
}}>
// Update the index for expanded groups to be able to keep it open after a re-render
const updatedExpandedGroups =
props.group.isCollapsed ?
update(this.state.expandedGroups, { $push: [props.group.startIndex] }) :
update(this.state.expandedGroups, { $splice: [[this.state.expandedGroups.indexOf(props.group.startIndex), 1]] });
this.setState({
expandedGroups: updatedExpandedGroups,
});
props.onToggleCollapse(props.group);
}}>
<div className="ms-Grid-col ms-u-sm1 ms-u-md1 ms-u-lg1">
<div className="header-icon">
<i className={ props.group.isCollapsed ? "ms-Icon ms-Icon--ChevronDown" : "ms-Icon ms-Icon--ChevronUp"}></i>
<i className={props.group.isCollapsed ? "ms-Icon ms-Icon--ChevronDown" : "ms-Icon ms-Icon--ChevronUp"}></i>
</div>
</div>
</div>
<div className="ms-Grid-col ms-u-sm10 ms-u-md10 ms-u-lg10">
<div className="ms-font-l">{ props.group.name }</div>
<div className="ms-font-l">{props.group.name}</div>
</div>
</div>
);
@ -211,7 +213,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
private _onClosePanel() {
this.setState({ showPanel: false });
}
private _onTogglePanel() {
this.setState({ showPanel: !this.state.showPanel });
}
@ -219,7 +221,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
private _addFilter(filterToAdd: IRefinementFilter): void {
// Add the filter to the selected filters collection
let newFilters = update(this.state.selectedFilters, {$push: [filterToAdd]});
let newFilters = update(this.state.selectedFilters, { $push: [filterToAdd] });
this._applyFilters(newFilters);
}
@ -239,9 +241,9 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
this.props.availableFilters.map((filter) => {
filter.Values.map((refinementValue: IRefinementValue, index) => {
allFilters.push({FilterName: filter.FilterName, Value: refinementValue});
});
filter.Values.map((refinementValue: IRefinementValue, index) => {
allFilters.push({ FilterName: filter.FilterName, Value: refinementValue });
});
});
this._applyFilters(allFilters);
@ -256,7 +258,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
* @param selectedFilters The filters to apply
*/
private _applyFilters(selectedFilters: IRefinementFilter[]): void {
// Save the selected filters
this.setState({
selectedFilters: selectedFilters,
@ -264,15 +266,15 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
this.props.onUpdateFilters(selectedFilters);
}
/**
* Checks if the current filter is present in the list of the selected filters
* @param filterToCheck The filter to check
*/
private _isInFilterSelection(filterToCheck: IRefinementFilter): boolean {
let newFilters = this.state.selectedFilters.filter((filter) => {
return filter.Value.RefinementToken === filterToCheck.Value.RefinementToken;
let newFilters = this.state.selectedFilters.filter((filter) => {
return filter.Value.RefinementToken === filterToCheck.Value.RefinementToken;
});
return newFilters.length === 0 ? false : true;

View File

@ -7,6 +7,8 @@ interface ISearchContainerProps {
selectedProperties: string[];
refiners: string;
showPaging: boolean;
showFileIcon: boolean;
showCreatedDate: boolean;
}
export default ISearchContainerProps;

View File

@ -41,6 +41,11 @@ interface ISearchContainerState {
* Indicates whether or not the componetn loads for the first time
*/
isComponentLoading: boolean;
/**
* Keeps the last query in case you change the query in the propery panel
*/
lastQuery: string;
}
export default ISearchContainerState;

View File

@ -12,18 +12,16 @@ 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> {
export default class SearchContainer extends React.Component<ISearchContainerProps, ISearchContainerState> {
public constructor(props) {
super(props);
// Set the initial state
this.state = {
results: {
RefinementResults: [],
RelevantResults: []
},
results: {
RefinementResults: [],
RelevantResults: []
},
selectedFilters: [],
availableFilters: [],
currentPage: 1,
@ -31,6 +29,7 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
isComponentLoading: true,
errorMessage: "",
hasError: false,
lastQuery: ""
};
this._onUpdateFilters = this._onUpdateFilters.bind(this);
@ -47,63 +46,64 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
let wpContent: JSX.Element = null;
let renderOverlay = null;
if (!isComponentLoading && areResultsLoading) {
renderOverlay = <div>
renderOverlay = <div>
<Overlay isDarkThemed={false} className="overlay">
</Overlay>
</div>;
}
if (isComponentLoading) {
wpContent = <Spinner size={ SpinnerSize.large } label={ strings.LoadingMessage } />;
wpContent = <Spinner size={SpinnerSize.large} label={strings.LoadingMessage} />;
} else {
if (hasError) {
wpContent = <MessageBar messageBarType= { MessageBarType.error }>{ errorMessage }</MessageBar>;
wpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
} else {
if (items.RelevantResults.length === 0) {
wpContent =
<div>
<FilterPanel availableFilters={ this.state.availableFilters } onUpdateFilters={ this._onUpdateFilters }/>
<div className="searchWp__noresult">{ strings.NoResultMessage }</div>
</div>;
} else {
wpContent =
<div>
<FilterPanel availableFilters={ this.state.availableFilters } onUpdateFilters={ this._onUpdateFilters }/>
{ renderOverlay }
<TilesList items={ items.RelevantResults }/>
{ this.props.showPaging ?
<Paging
totalItems={ items.TotalRows }
itemsCountPerPage={ this.props.maxResultsCount }
onPageUpdate={ this._onPageUpdate }
currentPage={ this.state.currentPage }/>
wpContent =
<div>
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} />
<div className="searchWp__noresult">{strings.NoResultMessage}</div>
</div>;
} else {
wpContent =
<div>
<FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} />
{renderOverlay}
<TilesList items={items.RelevantResults} showFileIcon={this.props.showFileIcon} showCreatedDate={this.props.showCreatedDate} />
{this.props.showPaging ?
<Paging
totalItems={items.TotalRows}
itemsCountPerPage={this.props.maxResultsCount}
onPageUpdate={this._onPageUpdate}
currentPage={this.state.currentPage} />
: null
}
</div>;
}
</div>;
}
}
}
}
return (
<div className="searchWp">
{ wpContent }
<div className="searchWp">
{wpContent}
</div>
);
}
public async componentDidMount() {
try {
this.setState({
areResultsLoading: true,
});
this.props.searchDataProvider.selectedProperties = this.props.selectedProperties;
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, this.props.refiners, this.state.selectedFilters, this.state.currentPage);
// Initial filters are just set once for the filter control during the component initialization
@ -114,6 +114,7 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
availableFilters: searchResults.RefinementResults,
areResultsLoading: false,
isComponentLoading: false,
lastQuery: this.props.queryKeywords + this.props.searchDataProvider.queryTemplate + this.props.selectedProperties.join(',')
});
} catch (error) {
@ -132,18 +133,22 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
public async componentWillReceiveProps(nextProps: ISearchContainerProps) {
let query = nextProps.queryKeywords + nextProps.searchDataProvider.queryTemplate + nextProps.selectedProperties.join(',');
// New props are passed to the component when the search query has been changed
if (this.props.refiners.toString() !== nextProps.refiners.toString()
|| this.props.maxResultsCount !== nextProps.maxResultsCount) {
|| this.props.maxResultsCount !== nextProps.maxResultsCount
|| this.state.lastQuery !== query
|| this.props.showFileIcon !== nextProps.showFileIcon
|| this.props.showCreatedDate !== nextProps.showCreatedDate ) {
try {
// Clear selected filters on a new query or new refiners
this.setState({
selectedFilters: [],
areResultsLoading: true,
});
this.props.searchDataProvider.selectedProperties = nextProps.selectedProperties;
// We reset the page number and refinement filters
const searchResults = await this.props.searchDataProvider.search(nextProps.queryKeywords, nextProps.refiners, [], 1);
@ -151,6 +156,7 @@ export default class SearchContainer extends React.Component<ISearchContainerPro
results: searchResults,
availableFilters: searchResults.RefinementResults,
areResultsLoading: false,
lastQuery: query
});
} catch (error) {

View File

@ -2,6 +2,8 @@ import { ISearchResult } from "../../../models/ISearchResult";
interface ITileProps {
item: ISearchResult;
showFileIcon: boolean;
showCreatedDate: boolean;
}
export default ITileProps;

View File

@ -2,6 +2,8 @@ import { ISearchResult } from "../../../models/ISearchResult";
interface ITilesListViewProps {
items?: ISearchResult[];
showFileIcon: boolean;
showCreatedDate: boolean;
}
export default ITilesListViewProps;

View File

@ -1,16 +1,17 @@
import * as React from "react";
import ITileProps from "./ITileProps";
import {
DocumentCard,
DocumentCardActions,
DocumentCardActivity,
DocumentCardLocation,
DocumentCardPreview,
DocumentCardTitle,
IDocumentCardPreviewProps
DocumentCard,
DocumentCardActions,
DocumentCardActivity,
DocumentCardLocation,
DocumentCardPreview,
DocumentCardTitle,
IDocumentCardPreviewProps
} from 'office-ui-fabric-react/lib/DocumentCard';
import { ImageFit } from 'office-ui-fabric-react/lib/Image';
import * as moment from "moment";
import { isEmpty } from '@microsoft/sp-lodash-subset';
import "../SearchWebPart.scss";
const PREVIEW_IMAGE_WIDTH: number = 204;
@ -21,28 +22,36 @@ export default class Tile extends React.Component<ITileProps, null> {
public render() {
const item = this.props.item;
let previewSrc = "";
if (!isEmpty(item.SiteLogo)) previewSrc = item.SiteLogo;
else if (!isEmpty(item.PreviewUrl)) previewSrc = item.PreviewUrl;
else if (!isEmpty(item.PictureThumbnailURL)) previewSrc = item.PictureThumbnailURL;
else if (!isEmpty(item.ServerRedirectedPreviewURL)) previewSrc = item.ServerRedirectedPreviewURL;
let iconSrc = this.props.showFileIcon ? item.iconSrc : "";
let previewProps: IDocumentCardPreviewProps = {
previewImages: [
{
url: item.ServerRedirectedURL ? item.ServerRedirectedURL : item.Path,
previewImageSrc: item.ServerRedirectedPreviewURL,
iconSrc: item.iconSrc,
previewImageSrc: previewSrc,
iconSrc: iconSrc,
imageFit: ImageFit.cover,
height: PREVIEW_IMAGE_HEIGHT,
}
],
};
};
return (
<DocumentCard onClickHref={ item.ServerRedirectedURL ? item.ServerRedirectedURL : item.Path } className="searchWp__resultCard">
<DocumentCard onClickHref={item.ServerRedirectedURL ? item.ServerRedirectedURL : item.Path} className="searchWp__resultCard">
<div className="searchWp__tile__iconContainer" style={{ "height": PREVIEW_IMAGE_HEIGHT }}>
<DocumentCardPreview { ...previewProps } />
</div>
<DocumentCardTitle title={ item.Title } shouldTruncate={ false } />
<div className="searchWp__tile__footer">
<span>{ moment(item.Created).isValid() ? moment(item.Created).format("L"): null }</span>
</div>
<DocumentCardTitle title={item.Title} shouldTruncate={false} />
<div className="searchWp__tile__footer" hidden={!this.props.showCreatedDate}>
<span>{moment(item.Created).isValid() ? moment(item.Created).format("L") : null}</span>
</div>
</DocumentCard>
);
}

View File

@ -22,26 +22,26 @@ export default class TilesList extends React.Component<ITilesListViewProps, null
this._getItemCountForPage = this._getItemCountForPage.bind(this);
this._getPageHeight = this._getPageHeight.bind(this);
}
public render() {
const items = this.props.items;
return (
<List
items={ items }
getItemCountForPage={ this._getItemCountForPage }
getPageHeight={ this._getPageHeight }
renderedWindowsAhead={ 4 }
return (
<List
items={items}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
renderedWindowsAhead={4}
className="searchWp__list"
onRenderCell={ (item, index) => (
<div className="searchWp__tile"
style={ {
width: (100 / this._columnCount) + '%',
} }>
<Tile key={ index } item= { item }/>
</div>
)}/>
onRenderCell={(item, index) => (
<div className="searchWp__tile"
style={{
width: (100 / this._columnCount) + '%',
}}>
<Tile key={index} item={item} showFileIcon={this.props.showFileIcon} showCreatedDate={this.props.showCreatedDate} />
</div>
)} />
);
}

View File

@ -14,6 +14,8 @@ define([], function() {
"ApplyAllFiltersLabel": "Apply all filters",
"RemoveAllFiltersLabel": "Remove all filters",
"ShowPagingLabel": "Show paging",
"ShowFileIconLabel": "Show file icons",
"ShowCreatedDateLabel": "Show created date",
"NoFilterConfiguredLabel": "No filter configured",
"SearchQueryPlaceHolderText": "Search query in KQL format",
"EmptyFieldErrorMessage": "This field cannot be empty",

View File

@ -14,6 +14,8 @@ define([], function() {
"ApplyAllFiltersLabel": "Appliquer tous les filters",
"RemoveAllFiltersLabel": "Supprimer tous les filtres",
"ShowPagingLabel": "Afficher la pagination",
"ShowFileIconLabel": "Afficher les icônes de fichier",
"ShowCreatedDateLabel": "Afficher la date de création",
"NoFilterConfiguredLabel": "Aucun filtre configuré",
"SearchQueryPlaceHolderText": "Requête de recherche au format KQL",
"EmptyFieldErrorMessage": "Ce champ ne peut pas être vide",

View File

@ -13,6 +13,8 @@ declare interface ISearchWebPartStrings {
ApplyAllFiltersLabel: string;
RemoveAllFiltersLabel: string;
ShowPagingLabel: string;
ShowFileIconLabel: string;
ShowCreatedDateLabel: string;
NoFilterConfiguredLabel: string;
SearchQueryPlaceHolderText: string;
EmptyFieldErrorMessage: string;

View File

@ -7,10 +7,19 @@
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}