[react-search-refiners] Bug fixes + added previews + best bets (#649)
* * Updated data source connection for searchbox WP * * Updated filters behavior. Now total count per refiner value is updated every time a new filter is selected like the default SharePoint behavior * Updated shimmers * Updated data sources binding logic to be more efficient * * Updated static CSS classes to use SPFx modules instead * Added previews for documents in the default list template * Fixed bug https://github.com/SharePoint/sp-dev-fx-webparts/issues/642. The current page state was never updated after a new search. * Fixed bug https://github.com/SharePoint/sp-dev-fx-webparts/issues/641. Now the result count is updated every time for each refiner. * * CSS fixes * * Removed unecessary lodash references * Added responsive behavior for iframes using https://www.npmjs.com/package/on-el-resize * * Updated README * Added hover effect on the result image * * Bug fix on videos * * Added best bets support * * Removed the jQuery reference. Replaced DOM manipulations by plain JS
This commit is contained in:
parent
1a7b1c1386
commit
910d377a77
|
@ -34,6 +34,7 @@ Version|Date|Comments
|
||||||
1.4 | May 10, 2018 | <ul><li>Added the query suggestions feature to the search box Web Part</li><li>Added the automatic translation for taxonomy filter values according to the current site locale.</li> <li>Added the option in the search box Web Part to send the query to an other page</ul>
|
1.4 | May 10, 2018 | <ul><li>Added the query suggestions feature to the search box Web Part</li><li>Added the automatic translation for taxonomy filter values according to the current site locale.</li> <li>Added the option in the search box Web Part to send the query to an other page</ul>
|
||||||
1.5 | Jul 2, 2018 | <ul><li>Added a templating feature for search results with Handlebars inspired by the [react-content-query-webpart](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-content-query-webpart) sample.</li><li>Upgraded to 1.5.1-plusbeta to use the new SPFx dynamic data feature instead of event aggregator for Web Parts communication.</li> <li>Code refactoring and reorganization.</ul>
|
1.5 | Jul 2, 2018 | <ul><li>Added a templating feature for search results with Handlebars inspired by the [react-content-query-webpart](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-content-query-webpart) sample.</li><li>Upgraded to 1.5.1-plusbeta to use the new SPFx dynamic data feature instead of event aggregator for Web Parts communication.</li> <li>Code refactoring and reorganization.</ul>
|
||||||
2.0.0.5 | Sept 18, 2018 | <ul><li>Upgraded to 1.6.0-plusbeta.</li><li>Added dynamic loading of parts needed in edit mode to reduce web part footprint.</li><li>Added configuration to sort.</li><li>Added option to set web part title.</li><li>Added result count tokens.</li><li>Added toggel to load/use handlebars helpers/moment.</li></ul>
|
2.0.0.5 | Sept 18, 2018 | <ul><li>Upgraded to 1.6.0-plusbeta.</li><li>Added dynamic loading of parts needed in edit mode to reduce web part footprint.</li><li>Added configuration to sort.</li><li>Added option to set web part title.</li><li>Added result count tokens.</li><li>Added toggel to load/use handlebars helpers/moment.</li></ul>
|
||||||
|
2.1.0.0 | 14 Oct, 2018 | <ul><li>Bug fixes ([#641](https://github.com/SharePoint/sp-dev-fx-webparts/issues/641),[#642](https://github.com/SharePoint/sp-dev-fx-webparts/issues/642))</li><li>Added document and Office 365 videos previews for the list template.</li><li>Added SharePoint best bets support.</li></ul>
|
||||||
|
|
||||||
## Disclaimer
|
## 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.**
|
**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.**
|
||||||
|
@ -113,6 +114,35 @@ This Web Part allows you change customize the way you display your search result
|
||||||
<img src="./images/edit_template.png"/>
|
<img src="./images/edit_template.png"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
#### Best bets
|
||||||
|
|
||||||
|
This WP supports SharePoint best bets via SharePoint query rules:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="./images/query_rules.png"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="./images/best_bets.png"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
#### Elements previews
|
||||||
|
|
||||||
|
Previews are available, **only for the list view**, for Office documents and Office 365 videos (not Microsoft Stream). The embed URL is directly taken from the `ServerRedirectedEmbedURL` managed property retrieved from the search results.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="./images/result_preview.png"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The WebPart must have the following selected properties in the configuration to get the preview feature work (they are set by default):
|
||||||
|
- ServerRedirectedPreviewURL
|
||||||
|
- ServerRedirectedURL
|
||||||
|
- contentclass
|
||||||
|
- ServerRedirectedEmbedURL
|
||||||
|
- DefaultEncodingURL
|
||||||
|
|
||||||
|
This preview is displayed as an _iframe_ when the user clicks on the corresponding preview image. DOM manipulations occur to add the _iframe_ container dynamically aside with the _<img/>_ container.
|
||||||
|
|
||||||
#### Available tokens ####
|
#### Available tokens ####
|
||||||
|
|
||||||
Setting | Description
|
Setting | Description
|
||||||
|
@ -144,5 +174,6 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
|
||||||
- Integrate the [@pnp/spfx-property-controls](https://github.com/SharePoint/sp-dev-fx-property-controls) in your solution (*PlaceHolder* control).
|
- Integrate the [@pnp/spfx-property-controls](https://github.com/SharePoint/sp-dev-fx-property-controls) in your solution (*PlaceHolder* control).
|
||||||
- Integrate multiple Office UI Fabric components (DocumentCard, Panel, GroupedList, ...) to fit with the native Office 365 theme.
|
- Integrate multiple Office UI Fabric components (DocumentCard, Panel, GroupedList, ...) to fit with the native Office 365 theme.
|
||||||
- Use the React container component approach inspiring by the [react-todo-basic sample](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-todo-basic).
|
- Use the React container component approach inspiring by the [react-todo-basic sample](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-todo-basic).
|
||||||
|
- Use [on-el-resize](https://www.npmjs.com/package/on-el-resize) by [Andrew Koltyakov](https://github.com/koltyakov) to resize iframes dynamically
|
||||||
|
|
||||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-search-refiners" />
|
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-search-refiners" />
|
|
@ -27,7 +27,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"externals": {},
|
|
||||||
"localizedResources": {
|
"localizedResources": {
|
||||||
"SearchWebPartStrings": "lib/webparts/searchResults/loc/{locale}.js",
|
"SearchWebPartStrings": "lib/webparts/searchResults/loc/{locale}.js",
|
||||||
"PropertyControlStrings": "./node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
"PropertyControlStrings": "./node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"solution": {
|
"solution": {
|
||||||
"name": "PnP - Search Web Parts",
|
"name": "PnP - Search Web Parts",
|
||||||
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||||
"version": "2.0.0.5",
|
"version": "2.1.0.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"skipFeatureDeployment": false,
|
"skipFeatureDeployment": false,
|
||||||
"features": [
|
"features": [
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"pageUrl": "https://localhost:5432/workbench"
|
"pageUrl": "https://localhost:5432/workbench"
|
||||||
},
|
},
|
||||||
"queryStringDataSource": {
|
"queryStringDataSource": {
|
||||||
"pageUrl": "https://collaborationcorner.sharepoint.com/teams/PnPIntranet/SitePages/Search(1).aspx",
|
"pageUrl": "https://collaborationcorner.sharepoint.com/teams/PnPIntranet/_layouts/15/workbench.aspx",
|
||||||
"customActions": {
|
"customActions": {
|
||||||
"24cae67d-dec7-4eff-bb41-49451d5b5a11": {
|
"24cae67d-dec7-4eff-bb41-49451d5b5a11": {
|
||||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 152 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "react-search-refiners",
|
"name": "react-search-refiners",
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
@ -33,17 +33,18 @@
|
||||||
"@types/sharepoint": "2013.1.9",
|
"@types/sharepoint": "2013.1.9",
|
||||||
"@types/webpack-env": "1.13.1",
|
"@types/webpack-env": "1.13.1",
|
||||||
"common-tags": "^1.8.0",
|
"common-tags": "^1.8.0",
|
||||||
"downshift": "^1.31.14",
|
"downshift": "1.31.14",
|
||||||
"handlebars": "^4.0.12",
|
"handlebars": "^4.0.12",
|
||||||
"handlebars-helpers": "^0.8.4",
|
"handlebars-helpers": "^0.8.4",
|
||||||
"immutability-helper": "2.4.0",
|
"immutability-helper": "2.4.0",
|
||||||
"lodash-es": "4.17.4",
|
"office-ui-fabric-react": "5.124.0",
|
||||||
"office-ui-fabric-react": "^5.124.0",
|
"on-el-resize": "0.0.4",
|
||||||
"react": "15.6.2",
|
"react": "15.6.2",
|
||||||
"react-ace": "^6.1.4",
|
"react-ace": "6.1.4",
|
||||||
"react-custom-scrollbars": "4.1.2",
|
"react-custom-scrollbars": "4.1.2",
|
||||||
"react-dom": "15.6.2",
|
"react-dom": "15.6.2",
|
||||||
"react-js-pagination": "3.0.0"
|
"react-js-pagination": "3.0.0",
|
||||||
|
"video.js": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/sp-build-web": "1.6.0-plusbeta",
|
"@microsoft/sp-build-web": "1.6.0-plusbeta",
|
||||||
|
|
|
@ -43,6 +43,10 @@ export default class QueryStringDataSourceApplicationCustomizer
|
||||||
|
|
||||||
this.context.dynamicDataSourceManager.initializeSource(this);
|
this.context.dynamicDataSourceManager.initializeSource(this);
|
||||||
|
|
||||||
|
if (this._searchQuery) {
|
||||||
|
this.context.dynamicDataSourceManager.notifySourceChanged();
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,24 +74,34 @@ export default class QueryStringDataSourceApplicationCustomizer
|
||||||
*/
|
*/
|
||||||
private _bindPushState() {
|
private _bindPushState() {
|
||||||
|
|
||||||
const _pushState = () => {
|
const _pushState = (() => {
|
||||||
const _defaultPushState = history.pushState;
|
const _defaultPushState = history.pushState;
|
||||||
const _self = this;
|
const _self = this;
|
||||||
return function (data: any, title: string, url?: string | null) {
|
return function (data: any, title: string, url?: string | null) {
|
||||||
|
|
||||||
const queryStringKeywords = UrlHelper.getQueryStringParam("q", url);
|
_self._updateQuery(_self, url);
|
||||||
|
|
||||||
if (queryStringKeywords && queryStringKeywords !== _self._searchQuery) {
|
|
||||||
_self._searchQuery = queryStringKeywords;
|
|
||||||
_self.context.dynamicDataSourceManager.notifyPropertyChanged('queryStringQuery');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the original function with the provided arguments
|
// Call the original function with the provided arguments
|
||||||
// This context is necessary for the context of the history change
|
// This context is necessary for the context of the history change
|
||||||
return _defaultPushState.apply(this, [data, title, url]);
|
return _defaultPushState.apply(this, [data, title, url]);
|
||||||
};
|
};
|
||||||
};
|
}).bind(this);
|
||||||
|
|
||||||
history.pushState = _pushState();
|
history.pushState = _pushState();
|
||||||
|
|
||||||
|
// Used when press the "back" button
|
||||||
|
window.onpopstate = ((ev: PopStateEvent) => {
|
||||||
|
this._updateQuery(this, ev.state.url);
|
||||||
|
}).bind(this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private _updateQuery(currentObject: QueryStringDataSourceApplicationCustomizer, url: string) {
|
||||||
|
|
||||||
|
const queryStringKeywords = UrlHelper.getQueryStringParam("q", url);
|
||||||
|
|
||||||
|
if (queryStringKeywords) {
|
||||||
|
currentObject._searchQuery = queryStringKeywords;
|
||||||
|
currentObject.context.dynamicDataSourceManager.notifyPropertyChanged("queryStringQuery");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Helper methods for plain JS DOM manipulations
|
||||||
|
* https://plainjs.com/javascript/
|
||||||
|
*/
|
||||||
|
export class DomHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over a list of DOM nodes (https://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack/)
|
||||||
|
* @param array the node list to browse
|
||||||
|
* @param callback the callback function
|
||||||
|
* @param scope the scope
|
||||||
|
*/
|
||||||
|
public static forEach(array, callback, scope?) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
callback.call(scope, i, array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a DOM element after an other
|
||||||
|
* @param el the dom element to insert
|
||||||
|
* @param referenceNode the parent node to insert after
|
||||||
|
*/
|
||||||
|
public static insertAfter(el, referenceNode) {
|
||||||
|
referenceNode.parentNode.insertBefore(el, referenceNode.nextSibling);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { IDynamicDataSource } from "@microsoft/sp-dynamic-data";
|
||||||
|
import { DynamicDataProvider } from "@microsoft/sp-component-base";
|
||||||
|
import IDynamicDataSourceConnection from "../models/IDynamicDataSourceConnection";
|
||||||
|
|
||||||
|
class DynamicDataHelper {
|
||||||
|
|
||||||
|
private _dynamicDataProvider: DynamicDataProvider;
|
||||||
|
private _instanceId: string;
|
||||||
|
private _componentId: string;
|
||||||
|
|
||||||
|
constructor(instanceId: string, componentId: string, dynamicDataProvider: DynamicDataProvider) {
|
||||||
|
this._instanceId = instanceId;
|
||||||
|
this._componentId = componentId;
|
||||||
|
this._dynamicDataProvider = dynamicDataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a data source from its instance id or component id
|
||||||
|
* @param dataSourceInstanceId the data source instance id
|
||||||
|
*/
|
||||||
|
public _tryGetSourceByInstanceOrComponentId(dataSourceConnection: IDynamicDataSourceConnection): IDynamicDataSource {
|
||||||
|
|
||||||
|
// Try get source by instance id
|
||||||
|
const availableSources = this._dynamicDataProvider.getAvailableSources();
|
||||||
|
|
||||||
|
let sources: IDynamicDataSource[] = availableSources.filter((item) => {
|
||||||
|
if (item.metadata.instanceId) {
|
||||||
|
// Instance id is always unique and doesn't change for Web Parts when refreshing the page
|
||||||
|
// This is not the case for extensions
|
||||||
|
if (item.metadata.instanceId.localeCompare(dataSourceConnection.instanceId) === 0
|
||||||
|
&& this._instanceId !== item.metadata.instanceId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sources.length === 0 ) {
|
||||||
|
|
||||||
|
// Try get source by component id instead (SPFx extension)
|
||||||
|
sources = availableSources.filter((item) => {
|
||||||
|
if (item.metadata.instanceId) {
|
||||||
|
|
||||||
|
if (item.metadata.componentId.localeCompare(dataSourceConnection.componentId) === 0
|
||||||
|
&& this._componentId !== item.metadata.componentId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sources.length > 0 ) {
|
||||||
|
return sources[0];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a data source connection object is initialized with all needed properties
|
||||||
|
* @param dataSourceConnection the data source connection
|
||||||
|
*/
|
||||||
|
public isDataSourceInstanceInitialized(dataSourceConnection: IDynamicDataSourceConnection): boolean {
|
||||||
|
return dataSourceConnection.instanceId && dataSourceConnection.sourceId && dataSourceConnection.propertyId && dataSourceConnection.componentId ? true : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicDataHelper;
|
|
@ -0,0 +1,24 @@
|
||||||
|
interface IDynamicDataSourceConnection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The source unique identifier
|
||||||
|
*/
|
||||||
|
sourceId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The proeprty id (i.e. "searchResultsCount")
|
||||||
|
*/
|
||||||
|
propertyId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The instance id
|
||||||
|
*/
|
||||||
|
instanceId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component id
|
||||||
|
*/
|
||||||
|
componentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IDynamicDataSourceConnection;
|
|
@ -1,6 +1,7 @@
|
||||||
export interface ISearchResults {
|
export interface ISearchResults {
|
||||||
RelevantResults: ISearchResult[];
|
RelevantResults: ISearchResult[];
|
||||||
RefinementResults: IRefinementResult[];
|
RefinementResults: IRefinementResult[];
|
||||||
|
PromotedResults?: IPromotedResult[];
|
||||||
TotalRows?: number;
|
TotalRows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +15,12 @@ export interface IRefinementResult {
|
||||||
Values: IRefinementValue[];
|
Values: IRefinementValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPromotedResult {
|
||||||
|
Url: string;
|
||||||
|
Title: string;
|
||||||
|
Description: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRefinementValue {
|
export interface IRefinementValue {
|
||||||
RefinementCount: number;
|
RefinementCount: number;
|
||||||
RefinementName: string;
|
RefinementName: string;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import ISearchService from './ISearchService';
|
import ISearchService from './ISearchService';
|
||||||
import { ISearchResults, IRefinementFilter, ISearchResult } from '../../models/ISearchResult';
|
import { ISearchResults, IRefinementFilter, ISearchResult } from '../../models/ISearchResult';
|
||||||
import intersection from 'lodash-es/intersection';
|
import { intersection, clone } from '@microsoft/sp-lodash-subset';
|
||||||
import clone from 'lodash-es/clone';
|
|
||||||
|
|
||||||
class MockSearchService implements ISearchService {
|
class MockSearchService implements ISearchService {
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import * as Handlebars from 'handlebars';
|
import * as Handlebars from 'handlebars';
|
||||||
import ISearchService from './ISearchService';
|
import ISearchService from './ISearchService';
|
||||||
import { ISearchResults, ISearchResult, IRefinementResult, IRefinementValue, IRefinementFilter } from '../../models/ISearchResult';
|
import { ISearchResults, ISearchResult, IRefinementResult, IRefinementValue, IRefinementFilter, IPromotedResult } from '../../models/ISearchResult';
|
||||||
import { sp, SearchQuery, SearchResults, SPRest, Web, Sort, SortDirection, SearchSuggestQuery } from '@pnp/sp';
|
import { sp, SearchQuery, SearchResults, SPRest, Sort, SortDirection, SearchSuggestQuery } from '@pnp/sp';
|
||||||
import { Logger, LogLevel, ConsoleListener } from '@pnp/logging';
|
import { Logger, LogLevel, ConsoleListener } from '@pnp/logging';
|
||||||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { Text } from '@microsoft/sp-core-library';
|
import { Text } from '@microsoft/sp-core-library';
|
||||||
import sortBy from 'lodash-es/sortBy';
|
import {sortBy, groupBy} from '@microsoft/sp-lodash-subset';
|
||||||
import groupBy from 'lodash-es/groupBy';
|
const mapKeys: any = require('lodash/mapKeys');
|
||||||
import mapValues from 'lodash-es/mapValues';
|
const mapValues: any = require('lodash/mapValues');
|
||||||
import mapKeys from 'lodash-es/mapKeys';
|
|
||||||
import LocalizationHelper from '../../helpers/LocalizationHelper';
|
import LocalizationHelper from '../../helpers/LocalizationHelper';
|
||||||
|
|
||||||
declare var System: any;
|
declare var System: any;
|
||||||
|
|
||||||
class SearchService implements ISearchService {
|
class SearchService implements ISearchService {
|
||||||
|
@ -218,6 +218,30 @@ class SearchService implements ISearchService {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Query rules handling
|
||||||
|
const secondaryQueryResults = r2.RawSearchResults.SecondaryQueryResults;
|
||||||
|
if (Array.isArray(secondaryQueryResults) && secondaryQueryResults.length > 0) {
|
||||||
|
|
||||||
|
let promotedResults: IPromotedResult[] = [];
|
||||||
|
|
||||||
|
secondaryQueryResults.map((e) => {
|
||||||
|
|
||||||
|
// Best bets are mapped through the "SpecialTermResults" https://msdn.microsoft.com/en-us/library/dd907265(v=office.12).aspx
|
||||||
|
if (e.SpecialTermResults) {
|
||||||
|
|
||||||
|
e.SpecialTermResults.Results.map((result) => {
|
||||||
|
promotedResults.push({
|
||||||
|
Title: result.Title,
|
||||||
|
Url: result.Url,
|
||||||
|
Description: result.Description
|
||||||
|
} as IPromotedResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
results.PromotedResults = promotedResults;
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve all the promises once to get news
|
// Resolve all the promises once to get news
|
||||||
const relevantResults: ISearchResult[] = await Promise.all(allItemsPromises);
|
const relevantResults: ISearchResult[] = await Promise.all(allItemsPromises);
|
||||||
|
|
||||||
|
@ -283,18 +307,17 @@ class SearchService implements ISearchService {
|
||||||
private async _mapToIcon(filename: string): Promise<string> {
|
private async _mapToIcon(filename: string): Promise<string> {
|
||||||
|
|
||||||
const webAbsoluteUrl = this._context.pageContext.web.absoluteUrl;
|
const webAbsoluteUrl = this._context.pageContext.web.absoluteUrl;
|
||||||
const web = new Web(webAbsoluteUrl);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encodedFileName = filename ? filename.replace(/['']/g, '') : '';
|
const encodedFileName = filename ? filename.replace(/['']/g, '') : '';
|
||||||
const iconFileName = await web.mapToIcon(encodedFileName, 1);
|
const iconFileName = await this._localPnPSetup.web.mapToIcon(encodedFileName, 1);
|
||||||
const iconUrl = webAbsoluteUrl + '/_layouts/15/images/' + iconFileName;
|
const iconUrl = webAbsoluteUrl + '/_layouts/15/images/' + iconFileName;
|
||||||
|
|
||||||
return iconUrl;
|
return iconUrl;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.write('[SharePointDataProvider._mapToIcon()]: Error: ' + error, LogLevel.Error);
|
Logger.write('[SearchService._mapToIcon()]: Error: ' + error, LogLevel.Error);
|
||||||
throw error;
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,12 +350,13 @@ class SearchService implements ISearchService {
|
||||||
let refinementQueryConditions: string[] = [];
|
let refinementQueryConditions: string[] = [];
|
||||||
let refinementQueryString: string = null;
|
let refinementQueryString: string = null;
|
||||||
|
|
||||||
|
// Conditions between values inside a refiner property
|
||||||
const refinementFilters = mapValues(groupBy(selectedFilters, 'FilterName'), (values) => {
|
const refinementFilters = mapValues(groupBy(selectedFilters, 'FilterName'), (values) => {
|
||||||
const refinementFilter = values.map((filter) => {
|
const refinementFilter = values.map((filter) => {
|
||||||
return filter.Value.RefinementToken;
|
return filter.Value.RefinementToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
return refinementFilter.length > 1 ? Text.format('or({0})', refinementFilter) : refinementFilter.toString();
|
return refinementFilter.length > 1 ? Text.format('and({0})', refinementFilter) : refinementFilter.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
mapKeys(refinementFilters, (value, key) => {
|
mapKeys(refinementFilters, (value, key) => {
|
||||||
|
@ -357,6 +381,7 @@ class SearchService implements ISearchService {
|
||||||
|
|
||||||
// Multiple filters
|
// Multiple filters
|
||||||
case (conditionsCount > 1): {
|
case (conditionsCount > 1): {
|
||||||
|
// Conditions between refiner properties
|
||||||
refinementQueryString = Text.format('and({0})', refinementQueryConditions.toString());
|
refinementQueryString = Text.format('and({0})', refinementQueryConditions.toString());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
.previewContainer {
|
||||||
|
|
||||||
|
&.videoPreview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.documentPreview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn {
|
||||||
|
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoverIcon {
|
||||||
|
color: '[theme:themeDarker, default:#0078d7]';
|
||||||
|
}
|
|
@ -4,6 +4,10 @@ import { html } from 'common-tags';
|
||||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||||
import * as strings from 'SearchWebPartStrings';
|
import * as strings from 'SearchWebPartStrings';
|
||||||
import { Text } from '@microsoft/sp-core-library';
|
import { Text } from '@microsoft/sp-core-library';
|
||||||
|
import 'video.js/dist/video-js.css';
|
||||||
|
import { Logger } from '@pnp/logging';
|
||||||
|
import templateStyles from './BaseTemplateService.module.scss';
|
||||||
|
import { DomHelper } from '../../helpers/DomHelper';
|
||||||
declare var System: any;
|
declare var System: any;
|
||||||
|
|
||||||
abstract class BaseTemplateService {
|
abstract class BaseTemplateService {
|
||||||
|
@ -35,21 +39,134 @@ abstract class BaseTemplateService {
|
||||||
*/
|
*/
|
||||||
public static getListDefaultTemplate(): string {
|
public static getListDefaultTemplate(): string {
|
||||||
return html`
|
return html`
|
||||||
|
<style>
|
||||||
|
.template_listItem {
|
||||||
|
display:flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
padding: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template_listItem img.img-preview {
|
||||||
|
width: 120px;
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
transition: .5s ease;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template_result {
|
||||||
|
display: flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template_listItem iframe, .template_listItem .video-js {
|
||||||
|
height: 250px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template_contentContainer {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template_previewContainer {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template_icon {
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover {
|
||||||
|
transition: .5s ease;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
-ms-transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-container:hover img {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-container:hover .hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div class="template_root">
|
<div class="template_root">
|
||||||
{{#if showResultsCount}}
|
{{#if showResultsCount}}
|
||||||
<div class="template_resultCount">
|
<div class="template_resultCount">
|
||||||
<label class="ms-fontWeight-semibold">{{getCountMessage totalRows keywords}}</label>
|
<label class="ms-fontWeight-semibold">{{getCountMessage totalRows keywords}}</label>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if promotedResults}}
|
||||||
|
<ul class="ms-List template_defaultList template_promotedResults">
|
||||||
|
<li class="ms-fontWeight-semibold title">{{strings.PromotedResultsLabel}}</li>
|
||||||
|
{{#each promotedResults as |promotedResult|}}
|
||||||
|
<li class="ms-ListItem-primaryText">
|
||||||
|
<div>
|
||||||
|
<i class="ms-Icon ms-Icon--MiniLink" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a class="ms-font-l" href="{{Url}}">{{Title}}</a>
|
||||||
|
<div class="ms-font-s">{{Description}}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
<ul class="ms-List template_defaultList">
|
<ul class="ms-List template_defaultList">
|
||||||
{{#each items as |item|}}
|
{{#each items as |item|}}
|
||||||
<li class="ms-ListItem ms-ListItem--image" tabindex="0">
|
<li class="template_listItem" tabindex="0">
|
||||||
<div class="ms-ListItem-image template_icon" style="background-image:url('{{iconSrc}}')">
|
<div class="template_result">
|
||||||
</div>
|
<img class="template_icon" src="{{iconSrc}}"/>
|
||||||
<span class="ms-ListItem-primaryText"><a href="{{getUrl item}}">{{Title}}</a></span>
|
<div class="template_contentContainer">
|
||||||
<span class="ms-ListItem-secondaryText">{{getSummary HitHighlightedSummary}}</span>
|
<span class=""><a href="{{getUrl item}}">{{Title}}</a></span>
|
||||||
<span class="ms-ListItem-tertiaryText">{{getDate Created "LL"}}</span>
|
<span class="">{{getSummary HitHighlightedSummary}}</span>
|
||||||
<div class="ms-ListItem-selectionTarget"></div>
|
<span class=""><span>{{getDate Created "LL"}}</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="template_previewContainer ms-hiddenSm">
|
||||||
|
{{#eq item.contentclass compare='STS_ListItem_851'}}
|
||||||
|
<div class="video-container">
|
||||||
|
<div class="img-container">
|
||||||
|
<img id="preview_{{@index}}" class="img-preview video-preview-item" src="{{PictureThumbnailURL}}" data-url="{{DefaultEncodingURL}}" data-fileext="{{FileType}}"/>
|
||||||
|
<div class="hover">
|
||||||
|
<div class="${templateStyles.hoverIcon}"><i class="ms-Icon ms-Icon--ImageSearch" aria-hidden="true"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/eq}}
|
||||||
|
|
||||||
|
{{#eq item.contentclass compare='STS_ListItem_DocumentLibrary'}}
|
||||||
|
{{#if ServerRedirectedPreviewURL}}
|
||||||
|
<div class="doc-container">
|
||||||
|
<div class="img-container">
|
||||||
|
<img id="preview_{{@index}}" class="img-preview document-preview-item" src="{{ServerRedirectedPreviewURL}}" data-url="{{ServerRedirectedEmbedURL}}"/>
|
||||||
|
<div class="hover">
|
||||||
|
<div class="${templateStyles.hoverIcon}"><i class="ms-Icon ms-Icon--ImageSearch" aria-hidden="true"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/eq}}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -207,9 +324,165 @@ abstract class BaseTemplateService {
|
||||||
return (pathExtension == '.htm' || pathExtension == '.html');
|
return (pathExtension == '.htm' || pathExtension == '.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the previews on search results for documents and videos. Called when a template is updated/changed
|
||||||
|
*/
|
||||||
|
public initPreviewElements(): void {
|
||||||
|
|
||||||
|
this._initVideoPreviews();
|
||||||
|
this._initDocumentPreviews();
|
||||||
|
}
|
||||||
|
|
||||||
public abstract getFileContent(fileUrl: string): Promise<string>;
|
public abstract getFileContent(fileUrl: string): Promise<string>;
|
||||||
|
|
||||||
public abstract ensureFileResolves(fileUrl: string): Promise<void>;
|
public abstract ensureFileResolves(fileUrl: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the preview HTML element to render depending on the file type
|
||||||
|
* @param containerId the container id
|
||||||
|
* @param closeButtonId the close button id to be able to bind events on it
|
||||||
|
* @param innerHtml the content to render inside the container depending the file type
|
||||||
|
*/
|
||||||
|
private _getPreviewContainerElement(containerId: string, closeButtonId: string, innerHtml: string, containerClass: string): string {
|
||||||
|
return `
|
||||||
|
<div id="${containerId}" class="${containerClass} ms-bgColor-neutralLighter"}>
|
||||||
|
<i id="${closeButtonId}" class="ms-Icon ms-Icon--ChromeClose ${templateStyles.closeBtn}" aria-hidden="true"></i>
|
||||||
|
${innerHtml}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _initDocumentPreviews() {
|
||||||
|
|
||||||
|
const nodes = document.querySelectorAll('.document-preview-item');
|
||||||
|
|
||||||
|
DomHelper.forEach(nodes, ((index, el) => {
|
||||||
|
el.addEventListener("click", (event) => {
|
||||||
|
const thumbnailElt = event.srcElement;
|
||||||
|
|
||||||
|
// Get infos about the video to render
|
||||||
|
const url = event.srcElement.getAttribute("data-url");
|
||||||
|
|
||||||
|
const iframeId = `document_${event.target.id}`; // ex: 'document-preview-itemXXX';
|
||||||
|
const previewContainedId = `${iframeId}_container`;
|
||||||
|
let containerElt = document.getElementById(previewContainedId);
|
||||||
|
|
||||||
|
if (containerElt) {
|
||||||
|
thumbnailElt.parentElement.style.display= 'none';
|
||||||
|
containerElt.style.display= '';
|
||||||
|
} else {
|
||||||
|
if (url) {
|
||||||
|
|
||||||
|
thumbnailElt.parentElement.style.display= 'none';
|
||||||
|
const closeBtnId = `${iframeId}_closeBtn`;
|
||||||
|
const innerPreviewHtml = `
|
||||||
|
<iframe id="${iframeId}" class="iframePreview" src="${url}" frameborder="0">
|
||||||
|
</iframe>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Build the preview HTML element
|
||||||
|
const previewHtml = this._getPreviewContainerElement(previewContainedId, closeBtnId, innerPreviewHtml, `${templateStyles.previewContainer} ${templateStyles.documentPreview}`);
|
||||||
|
const newEl = document.createElement('div');
|
||||||
|
newEl.innerHTML = previewHtml;
|
||||||
|
DomHelper.insertAfter(newEl, thumbnailElt.parentElement);
|
||||||
|
|
||||||
|
document.getElementById(closeBtnId).addEventListener("click", ((event) => {
|
||||||
|
thumbnailElt.parentElement.style.display= '';
|
||||||
|
document.getElementById(previewContainedId).style.display= 'none';
|
||||||
|
}).bind(containerElt, thumbnailElt));
|
||||||
|
} else {
|
||||||
|
Logger.write(`The URL of the video was empty for the document. Make sure you've included the 'ServerRedirectedEmbedURL' property in the selected properties options in the Web Part property pane`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initVideoPreviews() {
|
||||||
|
|
||||||
|
// Load Videos-Js on Demand
|
||||||
|
// Webpack will create a other bundle loaded on demand just for this library
|
||||||
|
const videoJs = await import(
|
||||||
|
/* webpackChunkName: 'videos-js' */
|
||||||
|
'video.js',
|
||||||
|
);
|
||||||
|
|
||||||
|
const Video = videoJs.default;
|
||||||
|
|
||||||
|
const nodes = document.querySelectorAll('.video-preview-item');
|
||||||
|
|
||||||
|
DomHelper.forEach(nodes, ((index, el) => {
|
||||||
|
el.addEventListener("click", (event) => {
|
||||||
|
|
||||||
|
const thumbnailElt = event.srcElement;
|
||||||
|
|
||||||
|
// Get infos about the video to render
|
||||||
|
const url = event.srcElement.getAttribute("data-url");
|
||||||
|
const fileExtension = event.srcElement.getAttribute("data-fileext");
|
||||||
|
const thumbnailSrc = event.srcElement.getAttribute("src");
|
||||||
|
|
||||||
|
const playerId = `video_${event.target.id}`; // ex: 'video-preview-itemXXX';
|
||||||
|
const previewContainedId = `${playerId}_container`;
|
||||||
|
let containerElt = document.getElementById(previewContainedId);
|
||||||
|
|
||||||
|
let player = Video.getPlayer(`#${playerId}`);
|
||||||
|
|
||||||
|
// Case when the player is still registered in Video.js but does not exist in the DOM (due to page mode switch or tempalte update)
|
||||||
|
if (player && !document.getElementById(playerId)) {
|
||||||
|
|
||||||
|
// In this case, we simply delete the player instance and recreate it
|
||||||
|
player.dispose();
|
||||||
|
player = Video.getPlayer(`#${playerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove exiting instance if there is already a player registered with id
|
||||||
|
if (player) {
|
||||||
|
thumbnailElt.parentElement.style.display= 'none';
|
||||||
|
containerElt.style.display= '';
|
||||||
|
} else {
|
||||||
|
if (url && fileExtension) {
|
||||||
|
|
||||||
|
thumbnailElt.parentElement.style.display= 'none';
|
||||||
|
|
||||||
|
const closeBtnId = `${playerId}_closeBtn`;
|
||||||
|
|
||||||
|
const innerPreviewHtml = `
|
||||||
|
<video id="${playerId}" class="video-js vjs-big-play-centered">
|
||||||
|
<source src="${url}" type="video/${fileExtension}">
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Build the preview HTML element
|
||||||
|
const previewHtml = this._getPreviewContainerElement(previewContainedId, closeBtnId, innerPreviewHtml, `${templateStyles.previewContainer} ${templateStyles.videoPreview}`);
|
||||||
|
const newEl = document.createElement('div');
|
||||||
|
newEl.innerHTML = previewHtml;
|
||||||
|
DomHelper.insertAfter(newEl, thumbnailElt.parentElement);
|
||||||
|
|
||||||
|
// Instantiate a new player with Video.js
|
||||||
|
const videoPlayer = new Video(playerId, {
|
||||||
|
controls: true,
|
||||||
|
autoplay: false,
|
||||||
|
preload: "metadata",
|
||||||
|
fluid: true,
|
||||||
|
poster: thumbnailSrc ? thumbnailSrc : null
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(closeBtnId).addEventListener("click", ((ev) => {
|
||||||
|
thumbnailElt.parentElement.style.display= '';
|
||||||
|
|
||||||
|
if(!videoPlayer.paused()) {
|
||||||
|
videoPlayer.pause();
|
||||||
|
}
|
||||||
|
document.getElementById(previewContainedId).style.display = 'none';
|
||||||
|
}).bind(videoPlayer, thumbnailElt));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Logger.write(`The URL of the video was empty for the video. Make sure you've included the 'DefaultEncodingURL' property in the selected properties options in the Web Part property pane`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseTemplateService;
|
export default BaseTemplateService;
|
|
@ -1,4 +1,5 @@
|
||||||
import { PageOpenBehavior } from '../../helpers/UrlHelper';
|
import { PageOpenBehavior } from '../../helpers/UrlHelper';
|
||||||
|
import IDynamicDataSourceConnection from '../../models/IDynamicDataSourceConnection';
|
||||||
|
|
||||||
interface ISearchBoxWebPartProps {
|
interface ISearchBoxWebPartProps {
|
||||||
searchInNewPage: boolean;
|
searchInNewPage: boolean;
|
||||||
|
@ -6,9 +7,7 @@ interface ISearchBoxWebPartProps {
|
||||||
openBehavior: PageOpenBehavior;
|
openBehavior: PageOpenBehavior;
|
||||||
enableQuerySuggestions: boolean;
|
enableQuerySuggestions: boolean;
|
||||||
useDynamicDataSource: boolean;
|
useDynamicDataSource: boolean;
|
||||||
dynamicDataSourceId: string;
|
sourceInstance: IDynamicDataSourceConnection;
|
||||||
dynamicDataSourcePropertyId: string;
|
|
||||||
dynamicDataSourceComponentId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ISearchBoxWebPartProps;
|
export default ISearchBoxWebPartProps;
|
|
@ -20,15 +20,16 @@ import { Log, Text, Environment, EnvironmentType } from
|
||||||
import ISearchService from '../../services/SearchService/ISearchService';
|
import ISearchService from '../../services/SearchService/ISearchService';
|
||||||
import MockSearchService from '../../services/SearchService/MockSearchService';
|
import MockSearchService from '../../services/SearchService/MockSearchService';
|
||||||
import SearchService from '../../services/SearchService/SearchService';
|
import SearchService from '../../services/SearchService/SearchService';
|
||||||
|
import DynamicDataHelper from '../../helpers/DynamicDataHelper';
|
||||||
const LOG_SOURCE: string = '[SearchBoxWebPart_{0}]';
|
|
||||||
|
|
||||||
export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWebPartProps> implements IDynamicDataCallables {
|
export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWebPartProps> implements IDynamicDataCallables {
|
||||||
|
|
||||||
|
private readonly LOG_SOURCE: string = '[SearchBoxWebPart_{0}]';
|
||||||
|
|
||||||
private _searchService: ISearchService;
|
private _searchService: ISearchService;
|
||||||
private _searchQuery: string;
|
private _searchQuery: string;
|
||||||
private _source: IDynamicDataSource;
|
private _source: IDynamicDataSource;
|
||||||
private _domElement: HTMLElement;
|
private _dynamicDataHelper: DynamicDataHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to be able to unregister dynamic data events if the source is updated
|
* Used to be able to unregister dynamic data events if the source is updated
|
||||||
|
@ -50,44 +51,37 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the connected data sources
|
* Binds data source properties to the Web Part properties. In some cases, the data source configuration is not retrieved propertly due to updated ids
|
||||||
* Useful in the case when the data source comes from an extension,
|
|
||||||
* the id is regenerated every time the page is refreshed causing the property pane configuration be lost
|
|
||||||
*/
|
*/
|
||||||
private _initDynamicDataSource() {
|
private _bindDataSources() {
|
||||||
|
|
||||||
if (this.properties.dynamicDataSourceId
|
const sourceFound = this._source ? true : false;
|
||||||
&& this.properties.dynamicDataSourcePropertyId
|
|
||||||
&& this.properties.dynamicDataSourceComponentId) {
|
|
||||||
|
|
||||||
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSourceId);
|
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId && !sourceFound) {
|
||||||
let sourceId = undefined;
|
|
||||||
|
|
||||||
if (this._source) {
|
|
||||||
sourceId = this._source.id;
|
|
||||||
} else {
|
|
||||||
// Try to resolve the source and get its id by the name
|
|
||||||
this._source = this._tryGetSourceByComponentId(this.properties.dynamicDataSourceComponentId);
|
|
||||||
sourceId = this._source ? this._source.id : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceId) {
|
let sourceId = undefined;
|
||||||
this.context.dynamicDataProvider.registerPropertyChanged(sourceId, this.properties.dynamicDataSourcePropertyId, this._dataSourceUpdated);
|
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.sourceInstance.sourceId);
|
||||||
this._searchQuery = this._source.getPropertyValue(this.properties.dynamicDataSourcePropertyId);
|
|
||||||
|
if (this._source ) {
|
||||||
|
sourceId = this._source .id;
|
||||||
|
} else {
|
||||||
|
this._source = this._dynamicDataHelper._tryGetSourceByInstanceOrComponentId(this.properties.sourceInstance);
|
||||||
|
sourceId = this._source ? this._source.id : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the property for the property pane
|
if (sourceId) {
|
||||||
this.properties.dynamicDataSourceId = sourceId;
|
this.context.dynamicDataProvider.registerPropertyChanged(sourceId, this.properties.sourceInstance.propertyId, this.render);
|
||||||
this._lastSourceId = this.properties.dynamicDataSourceId;
|
|
||||||
this._lastPropertyId = this.properties.dynamicDataSourcePropertyId;
|
|
||||||
|
|
||||||
// Notify subscriber of the initial value
|
// Update the property for the property pane
|
||||||
this.context.dynamicDataSourceManager.notifyPropertyChanged('inputQuery');
|
this.properties.sourceInstance.sourceId = sourceId;
|
||||||
|
this._lastSourceId = sourceId;
|
||||||
// If false, means the onInit method is not completed yet so we let it render the web part through the normal process
|
this._lastPropertyId = this.properties.sourceInstance.propertyId;
|
||||||
if (this.renderedOnce) {
|
|
||||||
this.render();
|
// If false, means the onInit method is not completed yet so we let it render the web part through the normal process
|
||||||
}
|
if (this.renderedOnce) {
|
||||||
}
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,60 +89,64 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
||||||
* Determines the group fields for the search query options inside the property pane
|
* Determines the group fields for the search query options inside the property pane
|
||||||
*/
|
*/
|
||||||
private _getSearchQueryFields(): IPropertyPaneField<any>[] {
|
private _getSearchQueryFields(): IPropertyPaneField<any>[] {
|
||||||
|
|
||||||
|
// Sets up search query fields
|
||||||
|
let searchQueryConfigFields: IPropertyPaneField<any>[] = [
|
||||||
|
PropertyPaneCheckbox('useDynamicDataSource', {
|
||||||
|
checked: false,
|
||||||
|
text: strings.UseDynamicDataSourceLabel,
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
// Sets up search query fields
|
if (this.properties.useDynamicDataSource) {
|
||||||
let searchQueryConfigFields: IPropertyPaneField<any>[] = [
|
const sourceOptions: IPropertyPaneDropdownOption[] =
|
||||||
PropertyPaneCheckbox('useDynamicDataSource', {
|
this.context.dynamicDataProvider.getAvailableSources().map(source => {
|
||||||
checked: false,
|
return {
|
||||||
text: strings.UseDynamicDataSourceLabel,
|
key: source.id,
|
||||||
})
|
text: source.metadata.title,
|
||||||
];
|
instanceId: source.metadata.instanceId,
|
||||||
|
componentId: source.metadata.componentId
|
||||||
if (this.properties.useDynamicDataSource) {
|
};
|
||||||
const sourceOptions: IPropertyPaneDropdownOption[] =
|
}).filter((item) => {
|
||||||
this.context.dynamicDataProvider.getAvailableSources().map(source => {
|
// We don't allow as data source:
|
||||||
return {
|
// - The component itself
|
||||||
key: source.id,
|
// - Components of the same type
|
||||||
text: source.metadata.title,
|
if (item.instanceId !== this.instanceId && this.componentId !== item.componentId) {
|
||||||
componentId: source.metadata.componentId
|
return item;
|
||||||
};
|
}
|
||||||
}).filter((item) => {
|
});
|
||||||
if (item.key.localeCompare("PageContext") !== 0 && item.componentId !== this.componentId) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedSource: string = this.properties.dynamicDataSourceId;
|
|
||||||
|
|
||||||
let propertyOptions: IPropertyPaneDropdownOption[] = [];
|
const selectedSource: string = this.properties.sourceInstance.sourceId;
|
||||||
if (selectedSource) {
|
let propertyOptions: IPropertyPaneDropdownOption[] = [];
|
||||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
|
||||||
if (source) {
|
if (selectedSource) {
|
||||||
propertyOptions = source.getPropertyDefinitions().map(prop => {
|
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
||||||
return {
|
if (source) {
|
||||||
key: prop.id,
|
propertyOptions = source.getPropertyDefinitions().map(prop => {
|
||||||
text: prop.title
|
return {
|
||||||
};
|
key: prop.id,
|
||||||
});
|
text: prop.title
|
||||||
}
|
};
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
searchQueryConfigFields = searchQueryConfigFields.concat([
|
searchQueryConfigFields = searchQueryConfigFields.concat([
|
||||||
PropertyPaneDropdown('dynamicDataSourceId', {
|
PropertyPaneDropdown('sourceInstance.sourceId', {
|
||||||
label: "Source",
|
label: strings.DynamicDataSourceLabel,
|
||||||
options: sourceOptions,
|
options: sourceOptions,
|
||||||
selectedKey: this.properties.dynamicDataSourceId,
|
selectedKey: this.properties.sourceInstance.sourceId,
|
||||||
}),
|
}),
|
||||||
PropertyPaneDropdown('dynamicDataSourcePropertyId', {
|
PropertyPaneDropdown('sourceInstance.propertyId', {
|
||||||
disabled: !this.properties.dynamicDataSourceId,
|
disabled: !this.properties.sourceInstance.sourceId,
|
||||||
label: "Source property",
|
label: strings.DynamicDataSourcePropertyLabel,
|
||||||
options: propertyOptions,
|
options: propertyOptions,
|
||||||
selectedKey: this.properties.dynamicDataSourcePropertyId
|
selectedKey: this.properties.sourceInstance.propertyId
|
||||||
}),
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchQueryConfigFields;
|
return searchQueryConfigFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -188,21 +186,6 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
||||||
return searchBehaviorOptionsFields;
|
return searchBehaviorOptionsFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler to notify data source subscribers the query string value has been updated
|
|
||||||
*/
|
|
||||||
private _dataSourceUpdated() {
|
|
||||||
|
|
||||||
if (this.properties.useDynamicDataSource) {
|
|
||||||
if (this.properties.dynamicDataSourceId && this.properties.dynamicDataSourcePropertyId) {
|
|
||||||
this._searchQuery = this._source ? this._source.getPropertyValue(this.properties.dynamicDataSourcePropertyId) : undefined;
|
|
||||||
this.context.dynamicDataSourceManager.notifyPropertyChanged('inputQuery');
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies if the string is a correct URL
|
* Verifies if the string is a correct URL
|
||||||
* @param value the URL to verify
|
* @param value the URL to verify
|
||||||
|
@ -216,28 +199,6 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a dynamic data source by its component id. The component id doesn't change when the page is refreshed
|
|
||||||
* @param dataSourceComponentId the component id
|
|
||||||
*/
|
|
||||||
private _tryGetSourceByComponentId(dataSourceComponentId: string): IDynamicDataSource {
|
|
||||||
const resolvedDataSource = this.context.dynamicDataProvider.getAvailableSources()
|
|
||||||
.filter((item) => {
|
|
||||||
if (item.metadata.componentId) {
|
|
||||||
if (item.metadata.componentId.localeCompare(dataSourceComponentId) === 0) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resolvedDataSource.length > 0 ) {
|
|
||||||
return resolvedDataSource[0];
|
|
||||||
} else {
|
|
||||||
Log.verbose(Text.format(LOG_SOURCE, "_tryGetSourceByComponentId()"), `Unable to find dynamic data source with componentId '${dataSourceComponentId}'`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the query suggestions data provider instance according to the current environnement
|
* Initializes the query suggestions data provider instance according to the current environnement
|
||||||
*/
|
*/
|
||||||
|
@ -253,50 +214,62 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the data source will be plugged in correctly when refreshing the whole page
|
||||||
|
* In the cas of extension, the source id changes every time so we need to set the correct suorce Id to corresponding property to get the value at render time
|
||||||
|
*/
|
||||||
|
private _reconnectDataSource() {
|
||||||
|
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId) {
|
||||||
|
this.context.dynamicDataProvider.registerAvailableSourcesChanged(this._bindDataSources.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected onInit(): Promise<void> {
|
protected onInit(): Promise<void> {
|
||||||
|
|
||||||
this._domElement = this.domElement;
|
this._source = undefined;
|
||||||
|
|
||||||
|
if(!this.properties.sourceInstance) {
|
||||||
|
this.properties.sourceInstance = {
|
||||||
|
componentId: null,
|
||||||
|
instanceId: null,
|
||||||
|
propertyId: null,
|
||||||
|
sourceId: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.initSearchService();
|
this.initSearchService();
|
||||||
|
|
||||||
this.context.dynamicDataSourceManager.initializeSource(this);
|
this.context.dynamicDataSourceManager.initializeSource(this);
|
||||||
|
|
||||||
// Make sure the data source will be plugged in correctly when loaded on the page
|
// Re bind data sources to WebPart properties
|
||||||
// Depending of the component loading order, some sources may be unavailable at this time so that's why we use an event listener
|
this._reconnectDataSource();
|
||||||
this.context.dynamicDataProvider.registerAvailableSourcesChanged(this._initDynamicDataSource.bind(this));
|
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onPropertyPaneFieldChanged(changedProperty: string) {
|
protected onPropertyPaneFieldChanged(propertyPath: string) {
|
||||||
|
|
||||||
this.initSearchService();
|
this.initSearchService();
|
||||||
|
|
||||||
if (changedProperty === 'dynamicDataSourceId') {
|
if (propertyPath === 'sourceInstance.sourceId') {
|
||||||
|
|
||||||
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSourceId);
|
// Select the first property by default
|
||||||
|
this.properties.sourceInstance.propertyId =
|
||||||
this.properties.dynamicDataSourcePropertyId = this._source.getPropertyDefinitions()[0].id;
|
this.context.dynamicDataProvider.tryGetSource(this.properties.sourceInstance.sourceId).getPropertyDefinitions()[0].id;
|
||||||
this.properties.dynamicDataSourceComponentId = this._source.metadata.componentId;
|
|
||||||
|
|
||||||
// Unregister previous event listeners is the source is updated
|
|
||||||
if (this._lastSourceId && this._lastPropertyId) {
|
|
||||||
// Check if the source is still on the page so we can unregister
|
|
||||||
if (this.context.dynamicDataProvider.tryGetSource(this._lastSourceId)) {
|
|
||||||
this.context.dynamicDataProvider.unregisterPropertyChanged(this._lastSourceId, this._lastPropertyId, this.render);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.dynamicDataSourceId, this.properties.dynamicDataSourcePropertyId, this._dataSourceUpdated);
|
|
||||||
|
|
||||||
this._lastSourceId = this.properties.dynamicDataSourceId;
|
|
||||||
this._lastPropertyId = this.properties.dynamicDataSourcePropertyId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperty === 'useDynamicDataSource') {
|
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId) {
|
||||||
if (!this.properties.useDynamicDataSource) {
|
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.sourceInstance.sourceId, this.properties.sourceInstance.propertyId, this.render);
|
||||||
this.context.dynamicDataProvider.unregisterAvailableSourcesChanged(this._initDynamicDataSource.bind(this));
|
this._lastSourceId = this.properties.sourceInstance.sourceId;
|
||||||
}
|
this._lastPropertyId = this.properties.sourceInstance.propertyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._lastSourceId && this._lastPropertyId) {
|
||||||
|
|
||||||
|
// In the case of extension, we don't need to unregister because the id changes every time the page is reloaded so it doesn't exist anymore
|
||||||
|
if (!this._lastSourceId.startsWith("Extension")) {
|
||||||
|
this.context.dynamicDataProvider.unregisterPropertyChanged(this._lastSourceId, this._lastPropertyId, this.render);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +314,22 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
||||||
|
|
||||||
public render(): void {
|
public render(): void {
|
||||||
|
|
||||||
|
if (this.properties.useDynamicDataSource) {
|
||||||
|
|
||||||
|
const needsConfiguration: boolean = !this.properties.sourceInstance.sourceId || !this.properties.sourceInstance.propertyId;
|
||||||
|
|
||||||
|
if (!needsConfiguration) {
|
||||||
|
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(this.properties.sourceInstance.sourceId);
|
||||||
|
let sourceValue = source ? source.getPropertyValue(this.properties.sourceInstance.propertyId) : undefined;
|
||||||
|
|
||||||
|
if (typeof sourceValue === 'string') {
|
||||||
|
this._searchQuery = sourceValue ? sourceValue : "";
|
||||||
|
} else {
|
||||||
|
Log.warn(Text.format(this.LOG_SOURCE, this.instanceId), `The selected input value from the dynamic data source is not a string. Received (${typeof sourceValue})`, this.context.serviceScope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const element: React.ReactElement<ISearchBoxContainerProps> = React.createElement(
|
const element: React.ReactElement<ISearchBoxContainerProps> = React.createElement(
|
||||||
SearchBox, {
|
SearchBox, {
|
||||||
onSearch: this._onSearch,
|
onSearch: this._onSearch,
|
||||||
|
@ -352,6 +341,6 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
||||||
searchService: this._searchService
|
searchService: this._searchService
|
||||||
} as ISearchBoxContainerProps);
|
} as ISearchBoxContainerProps);
|
||||||
|
|
||||||
ReactDom.render(element, this._domElement);
|
ReactDom.render(element, this.domElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { IconType } from 'office-ui-fabric-react/lib
|
||||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||||
import * as update from 'immutability-helper';
|
import * as update from 'immutability-helper';
|
||||||
import '../SearchBoxWebPart.scss';
|
import styles from '../SearchBoxWebPart.module.scss';
|
||||||
|
|
||||||
const SUGGESTION_CHAR_COUNT_TRIGGER = 3;
|
const SUGGESTION_CHAR_COUNT_TRIGGER = 3;
|
||||||
|
|
||||||
|
@ -125,9 +125,9 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
||||||
let suggestions: JSX.Element[] = null;
|
let suggestions: JSX.Element[] = null;
|
||||||
|
|
||||||
if (this.state.isRetrievingSuggestions && this.state.proposedQuerySuggestions.length === 0) {
|
if (this.state.isRetrievingSuggestions && this.state.proposedQuerySuggestions.length === 0) {
|
||||||
renderSuggestions = <div className="suggestionPanel">
|
renderSuggestions = <div className={styles.suggestionPanel}>
|
||||||
<div {...getItemProps({item: null, disabled: true})}>
|
<div {...getItemProps({item: null, disabled: true})}>
|
||||||
<div className="suggestionItem">
|
<div className={styles.suggestionItem}>
|
||||||
<Spinner size={ SpinnerSize.small }/>
|
<Spinner size={ SpinnerSize.small }/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,13 +142,13 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
||||||
style={{
|
style={{
|
||||||
fontWeight: selectedItem === suggestion ? 'bold' : 'normal'
|
fontWeight: selectedItem === suggestion ? 'bold' : 'normal'
|
||||||
}}>
|
}}>
|
||||||
<Label className={ highlightedIndex === index ? 'suggestionItem selected': 'suggestionItem'}>
|
<Label className={ highlightedIndex === index ? `${styles.suggestionItem} ${styles.selected}` : `${styles.suggestionItem}`}>
|
||||||
<div dangerouslySetInnerHTML={{ __html: suggestion }}></div>
|
<div dangerouslySetInnerHTML={{ __html: suggestion }}></div>
|
||||||
</Label>
|
</Label>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
renderSuggestions = <div className="suggestionPanel">
|
renderSuggestions = <div className={styles.suggestionPanel}>
|
||||||
{ suggestions }
|
{ suggestions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -285,7 +285,7 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="errorMessage">
|
className={styles.errorMessage}>
|
||||||
{ this.state.errorMessage }</MessageBar>;
|
{ this.state.errorMessage }</MessageBar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +293,7 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
||||||
this.renderSearchBoxWithAutoComplete() :
|
this.renderSearchBoxWithAutoComplete() :
|
||||||
this.renderBasicSearchBox();
|
this.renderBasicSearchBox();
|
||||||
return (
|
return (
|
||||||
<div className="searchBox">
|
<div className={styles.searchBox}>
|
||||||
{ renderErrorMessage }
|
{ renderErrorMessage }
|
||||||
{ renderSearchBox }
|
{ renderSearchBox }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,8 @@ define([], function() {
|
||||||
"SearchBoxPageOpenBehaviorLabel": "Opening behavior",
|
"SearchBoxPageOpenBehaviorLabel": "Opening behavior",
|
||||||
"SearchBoxDynamicPropertyInputLabel": "Input value",
|
"SearchBoxDynamicPropertyInputLabel": "Input value",
|
||||||
"UseDynamicDataSourceLabel": "Use a dynamic data source as search query",
|
"UseDynamicDataSourceLabel": "Use a dynamic data source as search query",
|
||||||
"SearchBoxDynamicDataSourceGroupName": "Dynamic data source configuration"
|
"SearchBoxDynamicDataSourceGroupName": "Dynamic data source configuration",
|
||||||
|
"DynamicDataSourceLabel": "Available data sources",
|
||||||
|
"DynamicDataSourcePropertyLabel": "Available properties"
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -10,6 +10,8 @@ define([], function() {
|
||||||
"SearchBoxPageOpenBehaviorLabel": "Mode d'ouverture de la page",
|
"SearchBoxPageOpenBehaviorLabel": "Mode d'ouverture de la page",
|
||||||
"SearchBoxDynamicPropertyInputLabel": "Valeur du champ de recherche",
|
"SearchBoxDynamicPropertyInputLabel": "Valeur du champ de recherche",
|
||||||
"UseDynamicDataSourceLabel": "Utiliser une source de données dynamique comme requête de recherche",
|
"UseDynamicDataSourceLabel": "Utiliser une source de données dynamique comme requête de recherche",
|
||||||
"SearchBoxDynamicDataSourceGroupName": "Configuration de la source de données dynamique"
|
"SearchBoxDynamicDataSourceGroupName": "Configuration de la source de données dynamique",
|
||||||
|
"DynamicDataSourceLabel": "Source de données disponibles",
|
||||||
|
"DynamicDataSourcePropertyLabel": "Propriétés disponibles"
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -13,6 +13,8 @@ declare interface ISearchBoxWebPartStrings {
|
||||||
SearchBoxDynamicPropertyInputLabel: string;
|
SearchBoxDynamicPropertyInputLabel: string;
|
||||||
UseDynamicDataSourceLabel: string;
|
UseDynamicDataSourceLabel: string;
|
||||||
SearchBoxDynamicDataSourceGroupName: string;
|
SearchBoxDynamicDataSourceGroupName: string;
|
||||||
|
DynamicDataSourceLabel: string;
|
||||||
|
DynamicDataSourcePropertyLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'SearchBoxWebPartStrings' {
|
declare module 'SearchBoxWebPartStrings' {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import ResultsLayoutOption from '../../models/ResultsLayoutOption';
|
import ResultsLayoutOption from '../../models/ResultsLayoutOption';
|
||||||
|
import IDynamicDataSourceConnection from '../../models/IDynamicDataSourceConnection';
|
||||||
|
|
||||||
export interface ISearchResultsWebPartProps {
|
export interface ISearchResultsWebPartProps {
|
||||||
queryKeywords: string;
|
queryKeywords: string;
|
||||||
|
@ -16,9 +17,7 @@ export interface ISearchResultsWebPartProps {
|
||||||
selectedLayout: ResultsLayoutOption;
|
selectedLayout: ResultsLayoutOption;
|
||||||
externalTemplateUrl: string;
|
externalTemplateUrl: string;
|
||||||
inlineTemplateText: string;
|
inlineTemplateText: string;
|
||||||
dynamicDataSourceId: string;
|
|
||||||
dynamicDataSourcePropertyId: string;
|
|
||||||
dynamicDataSourceComponentId: string;
|
|
||||||
useHandlebarsHelpers: boolean;
|
useHandlebarsHelpers: boolean;
|
||||||
webPartTitle: string;
|
webPartTitle: string;
|
||||||
|
sourceInstance: IDynamicDataSourceConnection;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"queryKeywords": "",
|
"queryKeywords": "",
|
||||||
"queryTemplate": "{searchTerms} Path:{Site}",
|
"queryTemplate": "{searchTerms} Path:{Site}",
|
||||||
"refiners": "Created:\"Created Date\",Size:\"Size of the file\"",
|
"refiners": "Created:\"Created Date\",Size:\"Size of the file\"",
|
||||||
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary",
|
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary,FileType,contentclass,ServerRedirectedEmbedURL,DefaultEncodingURL",
|
||||||
"enableQueryRules": false,
|
"enableQueryRules": false,
|
||||||
"maxResultsCount": 10,
|
"maxResultsCount": 10,
|
||||||
"showBlank": true,
|
"showBlank": true,
|
||||||
|
|
|
@ -37,21 +37,22 @@ import { update, isEmpty } from '@microsoft/sp-lodash-subset';
|
||||||
import MockTemplateService from '../../services/TemplateService/MockTemplateService';
|
import MockTemplateService from '../../services/TemplateService/MockTemplateService';
|
||||||
import BaseTemplateService from '../../services/TemplateService/BaseTemplateService';
|
import BaseTemplateService from '../../services/TemplateService/BaseTemplateService';
|
||||||
import { IDynamicDataSource } from '@microsoft/sp-dynamic-data';
|
import { IDynamicDataSource } from '@microsoft/sp-dynamic-data';
|
||||||
|
import DynamicDataHelper from '../../helpers/DynamicDataHelper';
|
||||||
|
|
||||||
declare var System: any;
|
declare var System: any;
|
||||||
|
|
||||||
const LOG_SOURCE: string = '[SearchResultsWebPart_{0}]';
|
|
||||||
|
|
||||||
export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchResultsWebPartProps> {
|
export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchResultsWebPartProps> {
|
||||||
|
|
||||||
|
private readonly LOG_SOURCE: string = '[SearchResultsWebPart_{0}]';
|
||||||
|
|
||||||
private _searchService: ISearchService;
|
private _searchService: ISearchService;
|
||||||
private _taxonomyService: ITaxonomyService;
|
private _taxonomyService: ITaxonomyService;
|
||||||
private _templateService: BaseTemplateService;
|
private _templateService: BaseTemplateService;
|
||||||
private _useResultSource: boolean;
|
private _useResultSource: boolean;
|
||||||
private _queryKeywords: string;
|
private _queryKeywords: string;
|
||||||
private _source: IDynamicDataSource;
|
private _source: IDynamicDataSource;
|
||||||
private _domElement: HTMLElement;
|
|
||||||
private _propertyPage = null;
|
private _propertyPage = null;
|
||||||
|
private _dynamicDataHelper: DynamicDataHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to be able to unregister dynamic data events if the source is updated
|
* Used to be able to unregister dynamic data events if the source is updated
|
||||||
|
@ -71,34 +72,31 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the connected data sources
|
* Binds data source properties to the Web Part properties. In some cases, the data source configuration is not retrieved propertly due to updated ids
|
||||||
* Useful in the case when the data source comes from an extension,
|
|
||||||
* the id is regenerated every time the page is refreshed causing the property pane configuration be lost
|
|
||||||
*/
|
*/
|
||||||
private _initDynamicDataSource() {
|
private _bindDataSources() {
|
||||||
|
|
||||||
if (this.properties.dynamicDataSourceId
|
const sourceFound = this._source ? true : false;
|
||||||
&& this.properties.dynamicDataSourcePropertyId
|
|
||||||
&& this.properties.dynamicDataSourceComponentId) {
|
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId && !sourceFound) {
|
||||||
|
|
||||||
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSourceId);
|
|
||||||
let sourceId = undefined;
|
let sourceId = undefined;
|
||||||
|
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.sourceInstance.sourceId);
|
||||||
if (this._source) {
|
|
||||||
sourceId = this._source.id;
|
if (this._source ) {
|
||||||
|
sourceId = this._source .id;
|
||||||
} else {
|
} else {
|
||||||
// Try to resolve the source and get its id by the name
|
this._source = this._dynamicDataHelper._tryGetSourceByInstanceOrComponentId(this.properties.sourceInstance);
|
||||||
this._source = this._tryGetSourceByComponentId(this.properties.dynamicDataSourceComponentId);
|
|
||||||
sourceId = this._source ? this._source.id : undefined;
|
sourceId = this._source ? this._source.id : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceId) {
|
if (sourceId) {
|
||||||
this.context.dynamicDataProvider.registerPropertyChanged(sourceId, this.properties.dynamicDataSourcePropertyId, this.render);
|
this.context.dynamicDataProvider.registerPropertyChanged(sourceId, this.properties.sourceInstance.propertyId, this.render);
|
||||||
|
|
||||||
// Update the property for the property pane
|
// Update the property for the property pane
|
||||||
this.properties.dynamicDataSourceId = sourceId;
|
this.properties.sourceInstance.sourceId = sourceId;
|
||||||
this._lastSourceId = this.properties.dynamicDataSourceId;
|
this._lastSourceId = sourceId;
|
||||||
this._lastPropertyId = this.properties.dynamicDataSourcePropertyId;
|
this._lastPropertyId = this.properties.sourceInstance.propertyId;
|
||||||
|
|
||||||
// If false, means the onInit method is not completed yet so we let it render the web part through the normal process
|
// If false, means the onInit method is not completed yet so we let it render the web part through the normal process
|
||||||
if (this.renderedOnce) {
|
if (this.renderedOnce) {
|
||||||
|
@ -109,27 +107,16 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a dynamic data source by its component id. The component id doesn't change when the page is refreshed
|
* Make sure the data source will be plugged in correctly when refreshing the whole page
|
||||||
* @param dataSourceComponentId the component id
|
* In the cas of extension, the source id changes every time so we need to set the correct suorce Id to corresponding property to get the value at render time
|
||||||
*/
|
*/
|
||||||
private _tryGetSourceByComponentId(dataSourceComponentId: string): IDynamicDataSource {
|
private _reconnectDataSource() {
|
||||||
const resolvedDataSource = this.context.dynamicDataProvider.getAvailableSources()
|
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId) {
|
||||||
.filter((item) => {
|
this.context.dynamicDataProvider.registerAvailableSourcesChanged(this._bindDataSources.bind(this));
|
||||||
if (item.metadata.componentId) {
|
|
||||||
if (item.metadata.componentId.localeCompare(dataSourceComponentId) === 0) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resolvedDataSource.length > 0) {
|
|
||||||
return resolvedDataSource[0];
|
|
||||||
} else {
|
|
||||||
Log.verbose(Text.format(LOG_SOURCE, "_tryGetSourceByComponentId()"), `Unable to find dynamic data source with componentId '${dataSourceComponentId}'`);
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the group fields for the search settings options inside the property pane
|
* Determines the group fields for the search settings options inside the property pane
|
||||||
*/
|
*/
|
||||||
|
@ -178,7 +165,15 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
resizable: true,
|
resizable: true,
|
||||||
value: this.properties.refiners,
|
value: this.properties.refiners,
|
||||||
deferredValidationTime: 300,
|
deferredValidationTime: 300,
|
||||||
})
|
}),
|
||||||
|
PropertyPaneSlider('maxResultsCount', {
|
||||||
|
label: strings.MaxResultsCount,
|
||||||
|
max: 50,
|
||||||
|
min: 1,
|
||||||
|
showValue: true,
|
||||||
|
step: 1,
|
||||||
|
value: 50,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return searchSettingsFields;
|
return searchSettingsFields;
|
||||||
|
@ -198,16 +193,25 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
|
|
||||||
if (this.properties.useSearchBoxQuery) {
|
if (this.properties.useSearchBoxQuery) {
|
||||||
const sourceOptions: IPropertyPaneDropdownOption[] =
|
const sourceOptions: IPropertyPaneDropdownOption[] =
|
||||||
this.context.dynamicDataProvider.getAvailableSources().map(source => {
|
this.context.dynamicDataProvider.getAvailableSources().map(source => {
|
||||||
return {
|
return {
|
||||||
key: source.id,
|
key: source.id,
|
||||||
text: source.metadata.title
|
text: source.metadata.title,
|
||||||
};
|
instanceId: source.metadata.instanceId,
|
||||||
}).filter(item => item.key.localeCompare("PageContext") !== 0);
|
componentId: source.metadata.componentId
|
||||||
|
};
|
||||||
const selectedSource: string = this.properties.dynamicDataSourceId;
|
}).filter((item) => {
|
||||||
|
// We don't allow as data source:
|
||||||
|
// - The component itself
|
||||||
|
// - Components of the same type
|
||||||
|
if (item.instanceId !== this.instanceId && this.componentId !== item.componentId) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSource: string = this.properties.sourceInstance.sourceId;
|
||||||
let propertyOptions: IPropertyPaneDropdownOption[] = [];
|
let propertyOptions: IPropertyPaneDropdownOption[] = [];
|
||||||
|
|
||||||
if (selectedSource) {
|
if (selectedSource) {
|
||||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
||||||
if (source) {
|
if (source) {
|
||||||
|
@ -221,17 +225,17 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQueryConfigFields = searchQueryConfigFields.concat([
|
searchQueryConfigFields = searchQueryConfigFields.concat([
|
||||||
PropertyPaneDropdown('dynamicDataSourceId', {
|
PropertyPaneDropdown('sourceInstance.sourceId', {
|
||||||
label: "Source",
|
label: strings.DynamicDataSourceLabel,
|
||||||
options: sourceOptions,
|
options: sourceOptions,
|
||||||
selectedKey: this.properties.dynamicDataSourceId,
|
selectedKey: this.properties.sourceInstance.sourceId,
|
||||||
}),
|
}),
|
||||||
PropertyPaneDropdown('dynamicDataSourcePropertyId', {
|
PropertyPaneDropdown('sourceInstance.propertyId', {
|
||||||
disabled: !this.properties.dynamicDataSourceId,
|
disabled: !this.properties.sourceInstance.sourceId,
|
||||||
label: "Source property",
|
label: strings.DynamicDataSourcePropertyLabel,
|
||||||
options: propertyOptions,
|
options: propertyOptions,
|
||||||
selectedKey: this.properties.dynamicDataSourcePropertyId
|
selectedKey: this.properties.sourceInstance.propertyId
|
||||||
}),
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -249,17 +253,6 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
searchQueryConfigFields.push(
|
|
||||||
PropertyPaneLabel('', { text: '' }), // dummy space
|
|
||||||
PropertyPaneSlider('maxResultsCount', {
|
|
||||||
label: strings.MaxResultsCount,
|
|
||||||
max: 50,
|
|
||||||
min: 1,
|
|
||||||
showValue: true,
|
|
||||||
step: 1,
|
|
||||||
value: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return searchQueryConfigFields;
|
return searchQueryConfigFields;
|
||||||
}
|
}
|
||||||
|
@ -514,7 +507,16 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
*/
|
*/
|
||||||
protected async onInit(): Promise<void> {
|
protected async onInit(): Promise<void> {
|
||||||
|
|
||||||
this._domElement = this.domElement;
|
this._dynamicDataHelper = new DynamicDataHelper(this.instanceId, this.componentId, this.context.dynamicDataProvider);
|
||||||
|
|
||||||
|
if(!this.properties.sourceInstance) {
|
||||||
|
this.properties.sourceInstance = {
|
||||||
|
componentId: null,
|
||||||
|
instanceId: null,
|
||||||
|
propertyId: null,
|
||||||
|
sourceId: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (Environment.type === EnvironmentType.Local) {
|
if (Environment.type === EnvironmentType.Local) {
|
||||||
this._searchService = new MockSearchService();
|
this._searchService = new MockSearchService();
|
||||||
|
@ -537,9 +539,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
// Set the default search results layout
|
// Set the default search results layout
|
||||||
this.properties.selectedLayout = this.properties.selectedLayout ? this.properties.selectedLayout : ResultsLayoutOption.List;
|
this.properties.selectedLayout = this.properties.selectedLayout ? this.properties.selectedLayout : ResultsLayoutOption.List;
|
||||||
|
|
||||||
// Make sure the data source will be plugged in correctly when loaded on the page
|
this._reconnectDataSource();
|
||||||
// Depending of the component loading order, some sources may be unavailable at this time so that's why we use an event listener
|
|
||||||
this.context.dynamicDataProvider.registerAvailableSourcesChanged(this._initDynamicDataSource.bind(this));
|
|
||||||
|
|
||||||
return super.onInit();
|
return super.onInit();
|
||||||
}
|
}
|
||||||
|
@ -564,8 +564,8 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
const searchContainer: React.ReactElement<ISearchContainerProps> = React.createElement(
|
const searchContainer: React.ReactElement<ISearchContainerProps> = React.createElement(
|
||||||
SearchContainer,
|
SearchContainer,
|
||||||
{
|
{
|
||||||
searchDataProvider: this._searchService,
|
searchService: this._searchService,
|
||||||
taxonomyDataProvider: this._taxonomyService,
|
taxonomyService: this._taxonomyService,
|
||||||
queryKeywords: this._queryKeywords,
|
queryKeywords: this._queryKeywords,
|
||||||
maxResultsCount: this.properties.maxResultsCount,
|
maxResultsCount: this.properties.maxResultsCount,
|
||||||
resultSourceId: this.properties.resultSourceId,
|
resultSourceId: this.properties.resultSourceId,
|
||||||
|
@ -596,7 +596,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
);
|
);
|
||||||
|
|
||||||
if ((this.properties.queryKeywords && !this.properties.useSearchBoxQuery) ||
|
if ((this.properties.queryKeywords && !this.properties.useSearchBoxQuery) ||
|
||||||
(this.properties.useSearchBoxQuery && this.properties.dynamicDataSourcePropertyId)) {
|
(this.properties.useSearchBoxQuery && this.properties.sourceInstance.sourceId)) {
|
||||||
renderElement = searchContainer;
|
renderElement = searchContainer;
|
||||||
} else {
|
} else {
|
||||||
if (this.displayMode === DisplayMode.Edit) {
|
if (this.displayMode === DisplayMode.Edit) {
|
||||||
|
@ -606,7 +606,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDom.render(renderElement, this._domElement);
|
ReactDom.render(renderElement, this.domElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
|
@ -654,20 +654,53 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onPropertyPaneFieldChanged(changedProperty: string) {
|
public async onPropertyPaneFieldChanged(propertyPath: string) {
|
||||||
|
|
||||||
if (changedProperty === 'useSearchBoxQuery') {
|
if (propertyPath === 'useSearchBoxQuery') {
|
||||||
|
|
||||||
if (!this.properties.useSearchBoxQuery) {
|
if (!this.properties.useSearchBoxQuery) {
|
||||||
|
|
||||||
// Reset source settings if we don't use search query
|
// Reset source settings if we don't use search query
|
||||||
this.properties.dynamicDataSourceId = undefined;
|
this.properties.sourceInstance.sourceId = undefined;
|
||||||
this.properties.dynamicDataSourcePropertyId = undefined;
|
this.properties.sourceInstance.propertyId = undefined;
|
||||||
this.context.dynamicDataProvider.unregisterAvailableSourcesChanged(this._initDynamicDataSource.bind(this));
|
this.properties.sourceInstance.instanceId = undefined;
|
||||||
|
this.properties.sourceInstance.componentId = undefined;
|
||||||
|
|
||||||
|
if (this._lastSourceId) {
|
||||||
|
if (!this._lastSourceId.startsWith("Extension")) {
|
||||||
|
this.context.dynamicDataProvider.unregisterPropertyChanged(this._lastSourceId, this._lastPropertyId, this.render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Reset search query
|
||||||
|
this.properties.queryKeywords = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (propertyPath === 'sourceInstance.sourceId') {
|
||||||
|
|
||||||
|
// Select the first property by default
|
||||||
|
this.properties.sourceInstance.propertyId =
|
||||||
|
this.context.dynamicDataProvider.tryGetSource(this.properties.sourceInstance.sourceId).getPropertyDefinitions()[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._lastSourceId && this._lastPropertyId) {
|
||||||
|
|
||||||
|
// In the case of extension, we don't need to unregister because the id changes every time the page is reloaded so it doesn't exist anymore
|
||||||
|
if (!this._lastSourceId.startsWith("Extension")) {
|
||||||
|
this.context.dynamicDataProvider.unregisterPropertyChanged(this._lastSourceId, this._lastPropertyId, this.render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId) {
|
||||||
|
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.sourceInstance.sourceId, this.properties.sourceInstance.propertyId, this.render);
|
||||||
|
this._lastSourceId = this.properties.sourceInstance.sourceId;
|
||||||
|
this._lastPropertyId = this.properties.sourceInstance.propertyId;
|
||||||
|
}
|
||||||
|
|
||||||
// Detect if the layout has been changed to custom...
|
// Detect if the layout has been changed to custom...
|
||||||
if (changedProperty === 'inlineTemplateText') {
|
if (propertyPath === 'inlineTemplateText') {
|
||||||
|
|
||||||
// Automatically switch the option to 'Custom' if a default template has been edited
|
// Automatically switch the option to 'Custom' if a default template has been edited
|
||||||
// (meaning the user started from a the list or tiles template)
|
// (meaning the user started from a the list or tiles template)
|
||||||
|
@ -679,27 +712,6 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperty === 'dynamicDataSourceId') {
|
|
||||||
|
|
||||||
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSourceId);
|
|
||||||
|
|
||||||
this.properties.dynamicDataSourcePropertyId = this._source.getPropertyDefinitions()[0].id;
|
|
||||||
this.properties.dynamicDataSourceComponentId = this._source.metadata.componentId;
|
|
||||||
|
|
||||||
// Unregister previous event listeners is the source is updated
|
|
||||||
if (this._lastSourceId && this._lastPropertyId) {
|
|
||||||
// Check if the source is still on the page so we can unregister
|
|
||||||
if (this.context.dynamicDataProvider.tryGetSource(this._lastSourceId)) {
|
|
||||||
this.context.dynamicDataProvider.unregisterPropertyChanged(this._lastSourceId, this._lastPropertyId, this.render);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.dynamicDataSourceId, this.properties.dynamicDataSourcePropertyId, this.render);
|
|
||||||
|
|
||||||
this._lastSourceId = this.properties.dynamicDataSourceId;
|
|
||||||
this._lastPropertyId = this.properties.dynamicDataSourcePropertyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._templateService.LoadHandlebarsHelpers(this.properties.useHandlebarsHelpers);
|
await this._templateService.LoadHandlebarsHelpers(this.properties.useHandlebarsHelpers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,8 +728,18 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
||||||
|
|
||||||
// If a source is selected, use the value from here
|
// If a source is selected, use the value from here
|
||||||
if (this.properties.useSearchBoxQuery) {
|
if (this.properties.useSearchBoxQuery) {
|
||||||
if (this.properties.dynamicDataSourceId && this.properties.dynamicDataSourcePropertyId) {
|
|
||||||
this._queryKeywords = this._source ? this._source.getPropertyValue(this.properties.dynamicDataSourcePropertyId) : this._queryKeywords;
|
const needsConfiguration: boolean = !this.properties.sourceInstance.sourceId || !this.properties.sourceInstance.propertyId;
|
||||||
|
|
||||||
|
if (!needsConfiguration) {
|
||||||
|
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(this.properties.sourceInstance.sourceId);
|
||||||
|
let sourceValue = source ? source.getPropertyValue(this.properties.sourceInstance.propertyId) : undefined;
|
||||||
|
|
||||||
|
if (typeof sourceValue === 'string') {
|
||||||
|
this._queryKeywords = sourceValue ? sourceValue : "";
|
||||||
|
} else {
|
||||||
|
Log.warn(Text.format(this.LOG_SOURCE, this.instanceId), `The selected input value from the dynamic data source is not a string. Received (${typeof sourceValue})`, this.context.serviceScope);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,10 @@ import IFilterPanelProps from './IFilte
|
||||||
import IFilterPanelState from './IFilterPanelState';
|
import IFilterPanelState from './IFilterPanelState';
|
||||||
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
||||||
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
|
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
|
||||||
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
|
||||||
import * as strings from 'SearchWebPartStrings';
|
import * as strings from 'SearchWebPartStrings';
|
||||||
import { IRefinementValue, IRefinementFilter } from '../../../../models/ISearchResult';
|
import { IRefinementValue, IRefinementFilter } from '../../../../models/ISearchResult';
|
||||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||||
import { Text } from '@microsoft/sp-core-library';
|
import { Text } from '@microsoft/sp-core-library';
|
||||||
import '../SearchResultsWebPart.scss';
|
|
||||||
import * as update from 'immutability-helper';
|
import * as update from 'immutability-helper';
|
||||||
import {
|
import {
|
||||||
GroupedList,
|
GroupedList,
|
||||||
|
@ -16,7 +14,8 @@ import {
|
||||||
IGroupDividerProps
|
IGroupDividerProps
|
||||||
} from 'office-ui-fabric-react/lib/components/GroupedList/index';
|
} from 'office-ui-fabric-react/lib/components/GroupedList/index';
|
||||||
import { Scrollbars } from 'react-custom-scrollbars';
|
import { Scrollbars } from 'react-custom-scrollbars';
|
||||||
import { ActionButton } from 'office-ui-fabric-react/lib/Button';
|
import {ActionButton, Link} from 'office-ui-fabric-react';
|
||||||
|
import styles from '../SearchResultsWebPart.module.scss';
|
||||||
|
|
||||||
export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> {
|
export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> {
|
||||||
|
|
||||||
|
@ -34,7 +33,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
this._addFilter = this._addFilter.bind(this);
|
this._addFilter = this._addFilter.bind(this);
|
||||||
this._removeFilter = this._removeFilter.bind(this);
|
this._removeFilter = this._removeFilter.bind(this);
|
||||||
this._isInFilterSelection = this._isInFilterSelection.bind(this);
|
this._isInFilterSelection = this._isInFilterSelection.bind(this);
|
||||||
this._applyAllfilters = this._applyAllfilters.bind(this);
|
|
||||||
this._removeAllFilters = this._removeAllFilters.bind(this);
|
this._removeAllFilters = this._removeAllFilters.bind(this);
|
||||||
this._onRenderHeader = this._onRenderHeader.bind(this);
|
this._onRenderHeader = this._onRenderHeader.bind(this);
|
||||||
this._onRenderCell = this._onRenderCell.bind(this);
|
this._onRenderCell = this._onRenderCell.bind(this);
|
||||||
|
@ -61,7 +59,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<div className='filterPanel__filterProperty'>
|
|
||||||
{
|
{
|
||||||
filter.Values.map((refinementValue: IRefinementValue, j) => {
|
filter.Values.map((refinementValue: IRefinementValue, j) => {
|
||||||
|
|
||||||
|
@ -84,7 +81,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -92,7 +88,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
const renderSelectedFilters: JSX.Element[] = this.state.selectedFilters.map((filter) => {
|
const renderSelectedFilters: JSX.Element[] = this.state.selectedFilters.map((filter) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label className='filter'>
|
<Label className={styles.filter}>
|
||||||
<i className='ms-Icon ms-Icon--ClearFilter' onClick={() => { this._removeFilter(filter); }}></i>
|
<i className='ms-Icon ms-Icon--ClearFilter' onClick={() => { this._removeFilter(filter); }}></i>
|
||||||
{filter.Value.RefinementName}
|
{filter.Value.RefinementName}
|
||||||
</Label>
|
</Label>
|
||||||
|
@ -103,7 +99,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
ref='groupedList'
|
ref='groupedList'
|
||||||
items={items}
|
items={items}
|
||||||
onRenderCell={this._onRenderCell}
|
onRenderCell={this._onRenderCell}
|
||||||
className='filterPanel__body__group'
|
className={styles.searchWp__filterPanel__body__group}
|
||||||
groupProps={
|
groupProps={
|
||||||
{
|
{
|
||||||
onRenderHeader: this._onRenderHeader,
|
onRenderHeader: this._onRenderHeader,
|
||||||
|
@ -111,64 +107,81 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
}
|
}
|
||||||
groups={groups} />;
|
groups={groups} />;
|
||||||
|
|
||||||
|
const renderLinkRemoveAll = this.state.selectedFilters.length > 0 ?
|
||||||
|
<Link onClick={this._removeAllFilters}>
|
||||||
|
{strings.RemoveAllFiltersLabel}
|
||||||
|
</Link> : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ActionButton
|
<div className="ms-textAlignRight">
|
||||||
className='searchWp__filterResultBtn'
|
<ActionButton
|
||||||
iconProps={{ iconName: 'Filter' }}
|
className={`${styles.searchWp__filterResultBtn} ms-fontWeight-semibold`}
|
||||||
text={strings.FilterResultsButtonLabel}
|
iconProps={{ iconName: 'Filter' }}
|
||||||
onClick={this._onTogglePanel}
|
text={strings.FilterResultsButtonLabel}
|
||||||
/>
|
onClick={this._onTogglePanel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{(this.state.selectedFilters.length > 0) ?
|
{(this.state.selectedFilters.length > 0) ?
|
||||||
|
|
||||||
<div className='searchWp__selectedFilters'>
|
<div className={styles.searchWp__selectedFilters}>
|
||||||
{renderSelectedFilters}
|
{renderSelectedFilters}
|
||||||
</div>
|
</div>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
<Panel
|
<Panel
|
||||||
className='filterPanel'
|
className={styles.searchWp__filterPanel}
|
||||||
isOpen={this.state.showPanel}
|
isOpen={this.state.showPanel}
|
||||||
type={PanelType.smallFixedNear}
|
type={PanelType.custom}
|
||||||
isBlocking={false}
|
customWidth="450px"
|
||||||
isLightDismiss={true}
|
isBlocking={false}
|
||||||
onDismiss={this._onClosePanel}
|
isLightDismiss={true}
|
||||||
headerText={strings.FilterPanelTitle}
|
onDismiss={this._onClosePanel}
|
||||||
closeButtonAriaLabel='Close'
|
headerText={strings.FilterPanelTitle}
|
||||||
hasCloseButton={true}
|
closeButtonAriaLabel='Close'
|
||||||
headerClassName='filterPanel__header'
|
hasCloseButton={true}
|
||||||
onRenderBody={() => {
|
onRenderBody={() => {
|
||||||
if (this.props.availableFilters.length > 0) {
|
if (this.props.availableFilters.length > 0) {
|
||||||
return (
|
return (
|
||||||
<Scrollbars style={{ height: '100%' }}>
|
<Scrollbars style={{height: '100%'}}>
|
||||||
<div className='filterPanel__body'>
|
<div className={styles.searchWp__filterPanel__body}>
|
||||||
<div className='filterPanel__body__allFiltersToggle'>
|
<div
|
||||||
<Toggle
|
className={`${styles.searchWp__filterPanel__body__allFiltersToggle} ${this.state.selectedFilters.length == 0 && "hiddenLink"}`}>
|
||||||
onText={strings.RemoveAllFiltersLabel}
|
{renderLinkRemoveAll}
|
||||||
offText={strings.ApplyAllFiltersLabel}
|
</div>
|
||||||
onChanged={(checked: boolean) => {
|
{renderAvailableFilters}
|
||||||
checked ? this._applyAllfilters() : this._removeAllFilters();
|
|
||||||
}}
|
|
||||||
checked={this.state.selectedFilters.length === 0 ? false : true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{renderAvailableFilters}
|
</Scrollbars>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={styles.searchWp__filterPanel__body}>
|
||||||
|
{strings.NoFilterConfiguredLabel}
|
||||||
</div>
|
</div>
|
||||||
</Scrollbars>
|
);
|
||||||
);
|
}
|
||||||
} else {
|
}}>
|
||||||
return (
|
</Panel>
|
||||||
<div className='filterPanel__body'>
|
|
||||||
{strings.NoFilterConfiguredLabel}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
</Panel>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.setState({
|
||||||
|
selectedFilters: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillReceiveProps(nextProps: IFilterPanelProps) {
|
||||||
|
|
||||||
|
if (nextProps.resetSelectedFilters) {
|
||||||
|
// Reset the selected filter on new query
|
||||||
|
this.setState({
|
||||||
|
selectedFilters: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _onRenderCell(nestingDepth: number, item: any, itemIndex: number) {
|
private _onRenderCell(nestingDepth: number, item: any, itemIndex: number) {
|
||||||
return (
|
return (
|
||||||
<div className='ms-Grid-row' data-selection-index={itemIndex}>
|
<div className='ms-Grid-row' data-selection-index={itemIndex}>
|
||||||
|
@ -181,28 +194,29 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
|
|
||||||
private _onRenderHeader(props: IGroupDividerProps): JSX.Element {
|
private _onRenderHeader(props: IGroupDividerProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
<div className={styles.searchWp__filterPanel__body__group__header}>
|
||||||
|
<div className='ms-Grid-row' onClick={() => {
|
||||||
|
|
||||||
<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]] });
|
||||||
|
|
||||||
// Update the index for expanded groups to be able to keep it open after a re-render
|
this.setState({
|
||||||
const updatedExpandedGroups =
|
expandedGroups: 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({
|
props.onToggleCollapse(props.group);
|
||||||
expandedGroups: updatedExpandedGroups,
|
}}>
|
||||||
});
|
<div className='ms-Grid-col ms-u-sm1 ms-u-md1 ms-u-lg1'>
|
||||||
|
<div className={styles.searchWp__filterPanel__body__headerIcon}>
|
||||||
props.onToggleCollapse(props.group);
|
<i className={props.group.isCollapsed ? 'ms-Icon ms-Icon--ChevronDown' : 'ms-Icon ms-Icon--ChevronUp'}></i>
|
||||||
}}>
|
</div>
|
||||||
<div className='ms-Grid-col ms-u-sm1 ms-u-md1 ms-u-lg1'>
|
</div>
|
||||||
<div className='header-icon'>
|
<div className='ms-Grid-col ms-u-sm10 ms-u-md10 ms-u-lg10'>
|
||||||
<i className={props.group.isCollapsed ? 'ms-Icon ms-Icon--ChevronDown' : 'ms-Icon ms-Icon--ChevronUp'}></i>
|
<div className='ms-font-l'>{props.group.name}</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -219,7 +233,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
private _addFilter(filterToAdd: IRefinementFilter): void {
|
private _addFilter(filterToAdd: IRefinementFilter): void {
|
||||||
|
|
||||||
// Add the filter to the selected filters collection
|
// 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);
|
this._applyFilters(newFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,20 +247,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
||||||
this._applyFilters(newFilters);
|
this._applyFilters(newFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _applyAllfilters(): void {
|
|
||||||
|
|
||||||
let allFilters: IRefinementFilter[] = [];
|
|
||||||
|
|
||||||
this.props.availableFilters.map((filter) => {
|
|
||||||
|
|
||||||
filter.Values.map((refinementValue: IRefinementValue, index) => {
|
|
||||||
allFilters.push({ FilterName: filter.FilterName, Value: refinementValue });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this._applyFilters(allFilters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeAllFilters(): void {
|
private _removeAllFilters(): void {
|
||||||
this._applyFilters([]);
|
this._applyFilters([]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ interface IFilterPanelProps {
|
||||||
availableFilters: IRefinementResult[];
|
availableFilters: IRefinementResult[];
|
||||||
refinersConfiguration: { [key: string]: string };
|
refinersConfiguration: { [key: string]: string };
|
||||||
onUpdateFilters: RefinementFilterOperationCallback;
|
onUpdateFilters: RefinementFilterOperationCallback;
|
||||||
|
resetSelectedFilters: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IFilterPanelProps;
|
export default IFilterPanelProps;
|
|
@ -1,10 +1,12 @@
|
||||||
import { ISearchResult } from '../../../../models/ISearchResult';
|
import { ISearchResult, IPromotedResult } from '../../../../models/ISearchResult';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handlebars template context for search results
|
* Handlebars template context for search results
|
||||||
*/
|
*/
|
||||||
interface ISearchResultsTemplateContext {
|
interface ISearchResultsTemplateContext {
|
||||||
items: ISearchResult[];
|
items: ISearchResult[];
|
||||||
|
promotedResults?: IPromotedResult[];
|
||||||
|
strings: ISearchWebPartStrings;
|
||||||
totalRows: number;
|
totalRows: number;
|
||||||
keywords: string;
|
keywords: string;
|
||||||
showResultsCount: boolean;
|
showResultsCount: boolean;
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
interface ISearchResultsTemplateState {
|
interface ISearchResultsTemplateState {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handlebar compiled template
|
||||||
|
*/
|
||||||
processedTemplate: string;
|
processedTemplate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,39 @@
|
||||||
.searchWp {
|
|
||||||
|
.template_root {
|
||||||
@import '~office-ui-fabric/dist/sass/Fabric.scss';
|
@import '~office-ui-fabric/dist/sass/Fabric.scss';
|
||||||
@import '~office-ui-fabric/dist/components/Label/Label.scss';
|
@import '~office-ui-fabric/dist/components/Label/Label.scss';
|
||||||
@import '~office-ui-fabric/dist/components/List/List.scss';
|
@import '~office-ui-fabric/dist/components/List/List.scss';
|
||||||
@import '~office-ui-fabric/dist/components/ListItem/ListItem.scss';
|
@import '~office-ui-fabric/dist/components/ListItem/ListItem.scss';
|
||||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/FabricCore.scss';
|
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/FabricCore.scss';
|
||||||
|
|
||||||
.template_defaultList {
|
.template_defaultList {
|
||||||
.template_icon {
|
|
||||||
background-position: top;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
strong {
|
strong {
|
||||||
color: "[theme: themePrimary]"
|
color: "[theme: themePrimary, default: #005a9e]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template_promotedResults {
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
list-style: none;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.template_defaultCard {
|
.template_defaultCard {
|
||||||
.singleCard {
|
.singleCard {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
@ -42,6 +62,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.template_resultCount {
|
.template_resultCount {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
|
@ -2,24 +2,46 @@ import React = require('react');
|
||||||
import ISearchResultsTemplateProps from './ISearchResultsTemplateProps';
|
import ISearchResultsTemplateProps from './ISearchResultsTemplateProps';
|
||||||
import ISearchResultsTemplateState from './ISearchResultsTemplateState';
|
import ISearchResultsTemplateState from './ISearchResultsTemplateState';
|
||||||
import './SearchResultsTemplate.scss';
|
import './SearchResultsTemplate.scss';
|
||||||
|
import { Resize } from 'on-el-resize';
|
||||||
|
import { DomHelper } from '../../../../helpers/DomHelper';
|
||||||
|
|
||||||
export default class SearchResultsTemplate extends React.Component<ISearchResultsTemplateProps, ISearchResultsTemplateState> {
|
export default class SearchResultsTemplate extends React.Component<ISearchResultsTemplateProps, ISearchResultsTemplateState> {
|
||||||
|
|
||||||
|
private parentRef: HTMLElement;
|
||||||
|
private resize: Resize;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.resize = new Resize();
|
||||||
this.state = {
|
this.state = {
|
||||||
processedTemplate: null
|
processedTemplate: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.onComponentResize = this.onComponentResize.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: this.state.processedTemplate }}></div>;
|
return <div ref={el => this.parentRef = el}>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: this.state.processedTemplate }}></div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.resize.removeResizeListener(this.parentRef, this.onComponentResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this._updateTemplate(this.props);
|
this._updateTemplate(this.props);
|
||||||
|
this.resize.addResizeListener(this.parentRef, this.onComponentResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
|
||||||
|
// Post render operations (previews on elements, etc.)
|
||||||
|
this.props.templateService.initPreviewElements();
|
||||||
|
this.onComponentResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillReceiveProps(nextProps: ISearchResultsTemplateProps) {
|
public componentWillReceiveProps(nextProps: ISearchResultsTemplateProps) {
|
||||||
|
@ -37,4 +59,14 @@ export default class SearchResultsTemplate extends React.Component<ISearchResult
|
||||||
processedTemplate: template
|
processedTemplate: template
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onComponentResize() {
|
||||||
|
|
||||||
|
// Resize iframes accordingly
|
||||||
|
const nodes = document.querySelectorAll(".iframePreview, .video-js");
|
||||||
|
|
||||||
|
DomHelper.forEach(nodes, (index, elt) => {
|
||||||
|
elt.style.width = Math.floor(this.parentRef.offsetWidth/2) + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import IPagingProps from './IPagingProps';
|
import IPagingProps from './IPagingProps';
|
||||||
import Pagination from 'react-js-pagination';
|
import Pagination from 'react-js-pagination';
|
||||||
|
import styles from '../SearchResultsWebPart.module.scss';
|
||||||
|
|
||||||
export default class Paging extends React.Component<IPagingProps, null> {
|
export default class Paging extends React.Component<IPagingProps, null> {
|
||||||
|
|
||||||
|
@ -13,15 +14,15 @@ export default class Paging extends React.Component<IPagingProps, null> {
|
||||||
public render(): React.ReactElement<IPagingProps> {
|
public render(): React.ReactElement<IPagingProps> {
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div className='searchWp__paginationContainer'>
|
<div className={styles.searchWp__paginationContainer}>
|
||||||
<div className='searchWp__paginationContainer__pagination'>
|
<div className={styles.searchWp__paginationContainer__pagination}>
|
||||||
<Pagination
|
<Pagination
|
||||||
activePage={this.props.currentPage}
|
activePage={this.props.currentPage}
|
||||||
firstPageText={<i className='ms-Icon ms-Icon--DoubleChevronLeft' aria-hidden='true'></i>}
|
firstPageText={<i className='ms-Icon ms-Icon--DoubleChevronLeft' aria-hidden='true'></i>}
|
||||||
lastPageText={<i className='ms-Icon ms-Icon--DoubleChevronRight' aria-hidden='true'></i>}
|
lastPageText={<i className='ms-Icon ms-Icon--DoubleChevronRight' aria-hidden='true'></i>}
|
||||||
prevPageText={<i className='ms-Icon ms-Icon--ChevronLeft' aria-hidden='true'></i>}
|
prevPageText={<i className='ms-Icon ms-Icon--ChevronLeft' aria-hidden='true'></i>}
|
||||||
nextPageText={<i className='ms-Icon ms-Icon--ChevronRight' aria-hidden='true'></i>}
|
nextPageText={<i className='ms-Icon ms-Icon--ChevronRight' aria-hidden='true'></i>}
|
||||||
activeLinkClass={ 'active' }
|
activeLinkClass={ styles.active }
|
||||||
itemsCountPerPage={ this.props.itemsCountPerPage }
|
itemsCountPerPage={ this.props.itemsCountPerPage }
|
||||||
totalItemsCount={ this.props.totalItems }
|
totalItemsCount={ this.props.totalItems }
|
||||||
pageRangeDisplayed={5}
|
pageRangeDisplayed={5}
|
||||||
|
|
|
@ -14,12 +14,12 @@ interface ISearchResultsContainerProps {
|
||||||
/**
|
/**
|
||||||
* The search data provider instance
|
* The search data provider instance
|
||||||
*/
|
*/
|
||||||
searchDataProvider: ISearchService;
|
searchService: ISearchService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The taxonomy data provider instance
|
* The taxonomy data provider instance
|
||||||
*/
|
*/
|
||||||
taxonomyDataProvider: ITaxonomyService;
|
taxonomyService: ITaxonomyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The search query keywords
|
* The search query keywords
|
||||||
|
|
|
@ -42,11 +42,6 @@ interface ISearchResultsContainerState {
|
||||||
*/
|
*/
|
||||||
areResultsLoading: boolean;
|
areResultsLoading: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Keeps the last query in case you change the query in the propery panel
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,16 +3,16 @@ import ISearchContainerProps from './ISearchResultsContainerProps';
|
||||||
import ISearchContainerState from './ISearchResultsContainerState';
|
import ISearchContainerState from './ISearchResultsContainerState';
|
||||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||||
import { Shimmer, ShimmerElementType as ElemType } from 'office-ui-fabric-react/lib/Shimmer';
|
import { Shimmer, ShimmerElementType as ElemType, ShimmerElementsGroup } from 'office-ui-fabric-react/lib/Shimmer';
|
||||||
import { Logger, LogLevel } from '@pnp/logging';
|
import { Logger, LogLevel } from '@pnp/logging';
|
||||||
import * as strings from 'SearchWebPartStrings';
|
import * as strings from 'SearchWebPartStrings';
|
||||||
import { IRefinementFilter, IRefinementValue, IRefinementResult } from '../../../../models/ISearchResult';
|
import { IRefinementFilter, IRefinementValue, IRefinementResult } from '../../../../models/ISearchResult';
|
||||||
import '../SearchResultsWebPart.scss';
|
|
||||||
import Paging from '../Paging/Paging';
|
import Paging from '../Paging/Paging';
|
||||||
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
||||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
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';
|
||||||
|
|
||||||
declare var System: any;
|
declare var System: any;
|
||||||
let FilterPanel = null;
|
let FilterPanel = null;
|
||||||
|
@ -33,7 +33,6 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
availableFilters: [],
|
availableFilters: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
areResultsLoading: false,
|
areResultsLoading: false,
|
||||||
isComponentLoading: true,
|
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
hasError: false,
|
hasError: false,
|
||||||
lastQuery: ''
|
lastQuery: ''
|
||||||
|
@ -49,90 +48,108 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
const items = this.state.results;
|
const items = this.state.results;
|
||||||
const hasError = this.state.hasError;
|
const hasError = this.state.hasError;
|
||||||
const errorMessage = this.state.errorMessage;
|
const errorMessage = this.state.errorMessage;
|
||||||
const isComponentLoading = this.state.isComponentLoading;
|
|
||||||
|
|
||||||
let renderWpContent: JSX.Element = null;
|
let renderWpContent: JSX.Element = null;
|
||||||
let renderOverlay: JSX.Element = null;
|
let renderOverlay: JSX.Element = null;
|
||||||
|
let renderWebPartTitle: JSX.Element = null;
|
||||||
|
|
||||||
if (!isComponentLoading && areResultsLoading) {
|
if (areResultsLoading) {
|
||||||
renderOverlay = <div>
|
|
||||||
<Overlay isDarkThemed={false} className='overlay'>
|
|
||||||
<Spinner size={SpinnerSize.medium} />
|
|
||||||
</Overlay>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let webPartTitle = null;
|
if (items.RelevantResults.length > 0) {
|
||||||
if (this.props.webPartTitle && this.props.webPartTitle.length > 0) {
|
renderOverlay = <div>
|
||||||
webPartTitle = <WebPartTitle title={this.props.webPartTitle} updateProperty={null} displayMode={DisplayMode.Read} />;
|
<Overlay isDarkThemed={false} className={styles.overlay}>
|
||||||
}
|
<Spinner size={SpinnerSize.medium} />
|
||||||
|
</Overlay>
|
||||||
if (isComponentLoading) {
|
</div>;
|
||||||
//renderWpContent = <Spinner size={SpinnerSize.large} label={strings.LoadingMessage} />;
|
|
||||||
renderWpContent = (<div>
|
|
||||||
<Shimmer isDataLoaded={!isComponentLoading} width={'75%'} style={{ marginBottom: "10px" }} />
|
|
||||||
<Shimmer isDataLoaded={!isComponentLoading} width={'90%'} style={{ marginBottom: "10px" }} />
|
|
||||||
<Shimmer isDataLoaded={!isComponentLoading} width={'50%'} />
|
|
||||||
</div>);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
renderWpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
|
|
||||||
} else {
|
} else {
|
||||||
|
let i = 0;
|
||||||
let filterPanel = this.state.availableFilters && this.state.availableFilters.length > 0 ? <FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} refinersConfiguration={this.props.refiners} /> : <span />;
|
let renderShimmerElements: JSX.Element[] = [];
|
||||||
|
while (i < 4) {
|
||||||
if (items.RelevantResults.length === 0) {
|
renderShimmerElements.push(
|
||||||
|
<Shimmer
|
||||||
if (!this.props.showBlank) {
|
customElementsGroup={this._getShimmerElements()}
|
||||||
renderWpContent =
|
width="100%"
|
||||||
<div>
|
style={{ marginBottom: "20px" }}
|
||||||
{webPartTitle}
|
/>);
|
||||||
{filterPanel}
|
i++;
|
||||||
<div className='searchWp__noresult'>{strings.NoResultMessage}</div>
|
|
||||||
</div>;
|
|
||||||
} else {
|
|
||||||
if (this.props.displayMode === DisplayMode.Edit) {
|
|
||||||
renderWpContent = <MessageBar messageBarType={MessageBarType.info}>{strings.ShowBlankEditInfoMessage}</MessageBar>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
renderWpContent =
|
|
||||||
<div>
|
|
||||||
{webPartTitle}
|
|
||||||
{filterPanel}
|
|
||||||
{renderOverlay}
|
|
||||||
<SearchResultsTemplate
|
|
||||||
templateService={this.props.templateService}
|
|
||||||
templateContent={this.props.templateContent}
|
|
||||||
templateContext={
|
|
||||||
{
|
|
||||||
items: this.state.results.RelevantResults,
|
|
||||||
totalRows: this.state.resultCount,
|
|
||||||
keywords: this.props.queryKeywords,
|
|
||||||
showResultsCount: this.props.showResultsCount,
|
|
||||||
siteUrl: this.props.context.pageContext.site.serverRelativeUrl,
|
|
||||||
webUrl: this.props.context.pageContext.web.serverRelativeUrl,
|
|
||||||
maxResultsCount: this.props.maxResultsCount,
|
|
||||||
actualResultsCount: items.RelevantResults.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{this.props.showPaging ?
|
|
||||||
<Paging
|
|
||||||
totalItems={items.TotalRows}
|
|
||||||
itemsCountPerPage={this.props.maxResultsCount}
|
|
||||||
onPageUpdate={this._onPageUpdate}
|
|
||||||
currentPage={this.state.currentPage} />
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderWpContent = <div>{ renderShimmerElements }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (this.props.webPartTitle && this.props.webPartTitle.length > 0) {
|
||||||
|
renderWebPartTitle = <WebPartTitle title={this.props.webPartTitle} updateProperty={null} displayMode={DisplayMode.Read} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
renderWpContent = <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const currentQuery = this.props.queryKeywords + this.props.searchService.queryTemplate + this.props.selectedProperties.join(',');
|
||||||
|
const renderFilterPanel = this.state.availableFilters && this.state.availableFilters.length > 0 ?
|
||||||
|
<FilterPanel
|
||||||
|
availableFilters={this.state.availableFilters}
|
||||||
|
onUpdateFilters={this._onUpdateFilters}
|
||||||
|
refinersConfiguration={this.props.refiners}
|
||||||
|
resetSelectedFilters={ this.state.lastQuery !== currentQuery ? true : false}
|
||||||
|
/> : <span />;
|
||||||
|
|
||||||
|
if (items.RelevantResults.length === 0) {
|
||||||
|
|
||||||
|
// Check if a search request has already been entered (to distinguish the first use scenario)
|
||||||
|
if (!this.props.showBlank && this.state.lastQuery && !this.state.areResultsLoading) {
|
||||||
|
renderWpContent =
|
||||||
|
<div>
|
||||||
|
{renderWebPartTitle}
|
||||||
|
{renderFilterPanel}
|
||||||
|
<div className={styles.searchWp__noresult}>{strings.NoResultMessage}</div>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
if (this.props.displayMode === DisplayMode.Edit && !areResultsLoading) {
|
||||||
|
renderWpContent = <MessageBar messageBarType={MessageBarType.info}>{strings.ShowBlankEditInfoMessage}</MessageBar>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
renderWpContent =
|
||||||
|
<div>
|
||||||
|
{renderWebPartTitle}
|
||||||
|
{renderFilterPanel}
|
||||||
|
{renderOverlay}
|
||||||
|
<SearchResultsTemplate
|
||||||
|
templateService={this.props.templateService}
|
||||||
|
templateContent={this.props.templateContent}
|
||||||
|
templateContext={
|
||||||
|
{
|
||||||
|
items: this.state.results.RelevantResults,
|
||||||
|
promotedResults: this.state.results.PromotedResults,
|
||||||
|
totalRows: this.state.resultCount,
|
||||||
|
keywords: this.props.queryKeywords,
|
||||||
|
showResultsCount: this.props.showResultsCount,
|
||||||
|
siteUrl: this.props.context.pageContext.site.serverRelativeUrl,
|
||||||
|
webUrl: this.props.context.pageContext.web.serverRelativeUrl,
|
||||||
|
maxResultsCount: this.props.maxResultsCount,
|
||||||
|
actualResultsCount: items.RelevantResults.length,
|
||||||
|
strings: strings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{this.props.showPaging ?
|
||||||
|
<Paging
|
||||||
|
totalItems={items.TotalRows}
|
||||||
|
itemsCountPerPage={this.props.maxResultsCount}
|
||||||
|
onPageUpdate={this._onPageUpdate}
|
||||||
|
currentPage={this.state.currentPage} />
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='searchWp'>
|
<div className={styles.searchWp}>
|
||||||
{renderWpContent}
|
{renderWpContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -148,11 +165,11 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
areResultsLoading: true,
|
areResultsLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.searchDataProvider.selectedProperties = this.props.selectedProperties;
|
this.props.searchService.selectedProperties = this.props.selectedProperties;
|
||||||
|
|
||||||
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
|
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
|
||||||
|
|
||||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, this.state.currentPage);
|
const searchResults = await this.props.searchService.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, this.state.currentPage);
|
||||||
const localizedFilters = await this._getLocalizedFilters(searchResults.RefinementResults);
|
const localizedFilters = await this._getLocalizedFilters(searchResults.RefinementResults);
|
||||||
|
|
||||||
if (localizedFilters && localizedFilters.length > 0) {
|
if (localizedFilters && localizedFilters.length > 0) {
|
||||||
|
@ -163,16 +180,12 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
FilterPanel = filterPanelComponent.FilterPanel;
|
FilterPanel = filterPanelComponent.FilterPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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({
|
this.setState({
|
||||||
results: searchResults,
|
results: searchResults,
|
||||||
resultCount: searchResults.TotalRows,
|
resultCount: searchResults.TotalRows,
|
||||||
availableFilters: localizedFilters,
|
availableFilters: localizedFilters,
|
||||||
areResultsLoading: false,
|
areResultsLoading: false,
|
||||||
isComponentLoading: false,
|
lastQuery: this.props.queryKeywords + this.props.searchService.queryTemplate + this.props.selectedProperties.join(',')
|
||||||
lastQuery: this.props.queryKeywords + this.props.searchDataProvider.queryTemplate + this.props.selectedProperties.join(',')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -181,7 +194,6 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
areResultsLoading: false,
|
areResultsLoading: false,
|
||||||
isComponentLoading: false,
|
|
||||||
results: { RefinementResults: [], RelevantResults: [] },
|
results: { RefinementResults: [], RelevantResults: [] },
|
||||||
hasError: true,
|
hasError: true,
|
||||||
errorMessage: error.message
|
errorMessage: error.message
|
||||||
|
@ -189,15 +201,14 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
areResultsLoading: false,
|
areResultsLoading: false
|
||||||
isComponentLoading: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentWillReceiveProps(nextProps: ISearchContainerProps) {
|
public async componentWillReceiveProps(nextProps: ISearchContainerProps) {
|
||||||
|
|
||||||
let query = nextProps.queryKeywords + nextProps.searchDataProvider.queryTemplate + nextProps.selectedProperties.join(',');
|
let query = nextProps.queryKeywords + nextProps.searchService.queryTemplate + nextProps.selectedProperties.join(',');
|
||||||
|
|
||||||
// New props are passed to the component when the search query has been changed
|
// New props are passed to the component when the search query has been changed
|
||||||
if (JSON.stringify(this.props.refiners) !== JSON.stringify(nextProps.refiners)
|
if (JSON.stringify(this.props.refiners) !== JSON.stringify(nextProps.refiners)
|
||||||
|
@ -216,15 +227,15 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
areResultsLoading: true,
|
areResultsLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.searchDataProvider.selectedProperties = nextProps.selectedProperties;
|
this.props.searchService.selectedProperties = nextProps.selectedProperties;
|
||||||
|
|
||||||
const refinerManagedProperties = Object.keys(nextProps.refiners).join(',');
|
const refinerManagedProperties = Object.keys(nextProps.refiners).join(',');
|
||||||
|
|
||||||
// We reset the page number and refinement filters
|
// We reset the page number and refinement filters
|
||||||
const searchResults = await this.props.searchDataProvider.search(nextProps.queryKeywords, 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);
|
||||||
|
|
||||||
if (FilterPanel == null && localizedFilters && localizedFilters.length > 0) {
|
if (FilterPanel === null && localizedFilters && localizedFilters.length > 0) {
|
||||||
const filterPanelComponent = await System.import(
|
const filterPanelComponent = await System.import(
|
||||||
/* webpackChunkName: 'search-filterpanel' */
|
/* webpackChunkName: 'search-filterpanel' */
|
||||||
'../FilterPanel'
|
'../FilterPanel'
|
||||||
|
@ -238,6 +249,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
resultCount: searchResults.TotalRows,
|
resultCount: searchResults.TotalRows,
|
||||||
availableFilters: localizedFilters,
|
availableFilters: localizedFilters,
|
||||||
areResultsLoading: false,
|
areResultsLoading: false,
|
||||||
|
currentPage: 1,
|
||||||
lastQuery: query
|
lastQuery: query
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -247,7 +259,6 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
areResultsLoading: false,
|
areResultsLoading: false,
|
||||||
isComponentLoading: false,
|
|
||||||
results: { RefinementResults: [], RelevantResults: [] },
|
results: { RefinementResults: [], RelevantResults: [] },
|
||||||
hasError: true,
|
hasError: true,
|
||||||
errorMessage: error.message
|
errorMessage: error.message
|
||||||
|
@ -255,8 +266,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
areResultsLoading: false,
|
areResultsLoading: false
|
||||||
isComponentLoading: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -293,10 +303,14 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
|
|
||||||
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
|
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
|
||||||
|
|
||||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, refinerManagedProperties, newFilters, 1);
|
const searchResults = await
|
||||||
|
this.props.searchService.search(this.props.queryKeywords, refinerManagedProperties, newFilters, 1);
|
||||||
|
const localizedFilters = await
|
||||||
|
this._getLocalizedFilters(searchResults.RefinementResults);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
results: searchResults,
|
results: searchResults,
|
||||||
|
availableFilters: localizedFilters,
|
||||||
areResultsLoading: false,
|
areResultsLoading: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -314,7 +328,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
|
|
||||||
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
|
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
|
||||||
|
|
||||||
const searchResults = await this.props.searchDataProvider.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, pageNumber);
|
const searchResults = await this.props.searchService.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, pageNumber);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
results: searchResults,
|
results: searchResults,
|
||||||
|
@ -359,8 +373,8 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
if (termsToLocalize.length > 0) {
|
if (termsToLocalize.length > 0) {
|
||||||
|
|
||||||
// Process all terms in a single JSOM call for performance purpose. In general JSOM is pretty slow so we try to limit the number of calls...
|
// Process all terms in a single JSOM call for performance purpose. In general JSOM is pretty slow so we try to limit the number of calls...
|
||||||
await this.props.taxonomyDataProvider.initialize();
|
await this.props.taxonomyService.initialize();
|
||||||
const termValues = await this.props.taxonomyDataProvider.getTermsById(termsToLocalize.map((t) => { return t.termId; }));
|
const termValues = await this.props.taxonomyService.getTermsById(termsToLocalize.map((t) => { return t.termId; }));
|
||||||
|
|
||||||
const termsEnumerator = termValues.getEnumerator();
|
const termsEnumerator = termValues.getEnumerator();
|
||||||
|
|
||||||
|
@ -432,4 +446,24 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
||||||
|
|
||||||
return updatedFilters;
|
return updatedFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getShimmerElements(): JSX.Element {
|
||||||
|
return <div style={{ display: 'flex' }}>
|
||||||
|
<ShimmerElementsGroup
|
||||||
|
shimmerElements={[
|
||||||
|
{ type: ElemType.line, width: 40, height: 40 },
|
||||||
|
{ type: ElemType.gap, width: 10, height: 40 }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ShimmerElementsGroup
|
||||||
|
flexWrap={true}
|
||||||
|
width="100%"
|
||||||
|
shimmerElements={[
|
||||||
|
{ type: ElemType.line, width: '100%', height: 10 },
|
||||||
|
{ type: ElemType.line, width: '75%', height: 10 },
|
||||||
|
{ type: ElemType.gap, width: '25%', height: 20 }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
.searchWp {
|
.searchWp {
|
||||||
|
|
||||||
|
min-height: 35px;
|
||||||
|
|
||||||
|
// Needed to avoid overlay overflow
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&__noresult {
|
&__noresult {
|
||||||
padding:10px;
|
padding:10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
|
||||||
border: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__filterResultBtn {
|
&__filterResultBtn {
|
||||||
color: "[theme: themePrimary]";
|
color: "[theme: themePrimary, default: #005a9e]";
|
||||||
}
|
}
|
||||||
|
|
||||||
&__selectedFilters {
|
&__selectedFilters {
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
i:hover {
|
i:hover {
|
||||||
color: "[theme: themePrimary]";
|
color: "[theme: themePrimary, default: #005a9e]";
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
|
||||||
.searchWp__paginationContainer__pagination {
|
&__pagination {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
@ -58,65 +58,69 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
float: left;
|
float: left;
|
||||||
color: "[theme: themePrimary]";
|
color: "[theme: themePrimary, default: #005a9e]";
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: "[theme: themePrimary]";
|
color: "[theme: themePrimary, default: #005a9e]";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: "[theme: themePrimary, default: #005a9e]";
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.active {
|
|
||||||
background-color: "[theme: themePrimary]";
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.filterPanel {
|
&__filterPanel {
|
||||||
|
|
||||||
&__body {
|
position: relative;
|
||||||
padding: 20px;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
.header-icon {
|
&__body {
|
||||||
text-align: right;
|
padding-right: 20px;
|
||||||
margin-bottom:8px;
|
padding-left: 20px;
|
||||||
|
overflow: auto;
|
||||||
.ms-Icon {
|
|
||||||
font-size: 16px;
|
&__headerIcon {
|
||||||
font-weight: 500;
|
text-align: right;
|
||||||
line-height: 1.5;
|
margin-top: 5px;
|
||||||
letter-spacing: 0.5px;
|
|
||||||
|
.ms-Icon {
|
||||||
&.ms-Icon--CalculatorSubtract, &.ms-Icon--CalculatorAddition {
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.ms-Icon--CalculatorSubtract, &.ms-Icon--CalculatorAddition {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
&__allFiltersToggle {
|
||||||
&__allFiltersToggle {
|
margin-bottom: 15px;
|
||||||
margin-bottom: 25px;
|
padding-left: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
&__group {
|
|
||||||
|
|
||||||
.ms-List-page~.ms-List-page {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
&__group {
|
||||||
padding: 10px;
|
|
||||||
|
&__header {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,7 +10,7 @@ define([], function() {
|
||||||
"NoResultMessage": "There are no results to show",
|
"NoResultMessage": "There are no results to show",
|
||||||
"RefinersFieldLabel": "Refiners",
|
"RefinersFieldLabel": "Refiners",
|
||||||
"FilterPanelTitle": "Available filters",
|
"FilterPanelTitle": "Available filters",
|
||||||
"FilterResultsButtonLabel": "Filter results",
|
"FilterResultsButtonLabel": "Filters",
|
||||||
"SelectedFiltersLabel": "Selected filters:",
|
"SelectedFiltersLabel": "Selected filters:",
|
||||||
"ApplyAllFiltersLabel": "Apply all filters",
|
"ApplyAllFiltersLabel": "Apply all filters",
|
||||||
"RemoveAllFiltersLabel": "Remove all filters",
|
"RemoveAllFiltersLabel": "Remove all filters",
|
||||||
|
@ -50,6 +50,9 @@ define([], function() {
|
||||||
"ErrorTemplateExtension": "The template must be a valid .htm or .html file",
|
"ErrorTemplateExtension": "The template must be a valid .htm or .html file",
|
||||||
"ErrorTemplateResolve": "Unable to resolve the specified template. Error details: '{0}'",
|
"ErrorTemplateResolve": "Unable to resolve the specified template. Error details: '{0}'",
|
||||||
"WebPartTitle": "Web part title",
|
"WebPartTitle": "Web part title",
|
||||||
"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",
|
||||||
|
"DynamicDataSourcePropertyLabel": "Available properties",
|
||||||
|
"PromotedResultsLabel": "Promoted result(s)"
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -10,7 +10,7 @@ define([], function() {
|
||||||
"NoResultMessage": "Il n'y a aucun résultat à afficher.",
|
"NoResultMessage": "Il n'y a aucun résultat à afficher.",
|
||||||
"RefinersFieldLabel": "Filtres",
|
"RefinersFieldLabel": "Filtres",
|
||||||
"FilterPanelTitle": "Filtres disponibles",
|
"FilterPanelTitle": "Filtres disponibles",
|
||||||
"FilterResultsButtonLabel": "Filtrer l'affichage",
|
"FilterResultsButtonLabel": "Filtrer",
|
||||||
"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",
|
||||||
|
@ -50,6 +50,9 @@ define([], function() {
|
||||||
"ErrorTemplateExtension": "Le file modèle doit être un fichier .htm ou .html valide",
|
"ErrorTemplateExtension": "Le file modèle doit être un fichier .htm ou .html valide",
|
||||||
"ErrorTemplateResolve": "Impossible de résoudre le fichier. Détails: '{0}'",
|
"ErrorTemplateResolve": "Impossible de résoudre le fichier. Détails: '{0}'",
|
||||||
"WebPartTitle": "Titre de web part",
|
"WebPartTitle": "Titre de web part",
|
||||||
"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",
|
||||||
|
"DynamicDataSourcePropertyLabel": "Propriétés disponibles",
|
||||||
|
"PromotedResultsLabel": "Résultat(s) promu(s)"
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -50,6 +50,9 @@ declare interface ISearchWebPartStrings {
|
||||||
ErrorTemplateResolve: string;
|
ErrorTemplateResolve: string;
|
||||||
WebPartTitle: string;
|
WebPartTitle: string;
|
||||||
HandlebarsHelpersDescription: string;
|
HandlebarsHelpersDescription: string;
|
||||||
|
DynamicDataSourceLabel: string;
|
||||||
|
DynamicDataSourcePropertyLabel: string;
|
||||||
|
PromotedResultsLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'SearchWebPartStrings' {
|
declare module 'SearchWebPartStrings' {
|
||||||
|
|
Loading…
Reference in New Issue