[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.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.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
|
||||
**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"/>
|
||||
</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 ####
|
||||
|
||||
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 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 [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" />
|
|
@ -27,7 +27,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"SearchWebPartStrings": "lib/webparts/searchResults/loc/{locale}.js",
|
||||
"PropertyControlStrings": "./node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"solution": {
|
||||
"name": "PnP - Search Web Parts",
|
||||
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"version": "2.0.0.5",
|
||||
"version": "2.1.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": false,
|
||||
"features": [
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"pageUrl": "https://localhost:5432/workbench"
|
||||
},
|
||||
"queryStringDataSource": {
|
||||
"pageUrl": "https://collaborationcorner.sharepoint.com/teams/PnPIntranet/SitePages/Search(1).aspx",
|
||||
"pageUrl": "https://collaborationcorner.sharepoint.com/teams/PnPIntranet/_layouts/15/workbench.aspx",
|
||||
"customActions": {
|
||||
"24cae67d-dec7-4eff-bb41-49451d5b5a11": {
|
||||
"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",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
@ -33,17 +33,18 @@
|
|||
"@types/sharepoint": "2013.1.9",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"common-tags": "^1.8.0",
|
||||
"downshift": "^1.31.14",
|
||||
"downshift": "1.31.14",
|
||||
"handlebars": "^4.0.12",
|
||||
"handlebars-helpers": "^0.8.4",
|
||||
"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-ace": "^6.1.4",
|
||||
"react-ace": "6.1.4",
|
||||
"react-custom-scrollbars": "4.1.2",
|
||||
"react-dom": "15.6.2",
|
||||
"react-js-pagination": "3.0.0"
|
||||
"react-js-pagination": "3.0.0",
|
||||
"video.js": "^7.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.6.0-plusbeta",
|
||||
|
|
|
@ -43,6 +43,10 @@ export default class QueryStringDataSourceApplicationCustomizer
|
|||
|
||||
this.context.dynamicDataSourceManager.initializeSource(this);
|
||||
|
||||
if (this._searchQuery) {
|
||||
this.context.dynamicDataSourceManager.notifySourceChanged();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -70,24 +74,34 @@ export default class QueryStringDataSourceApplicationCustomizer
|
|||
*/
|
||||
private _bindPushState() {
|
||||
|
||||
const _pushState = () => {
|
||||
const _pushState = (() => {
|
||||
const _defaultPushState = history.pushState;
|
||||
const _self = this;
|
||||
return function (data: any, title: string, url?: string | null) {
|
||||
|
||||
const queryStringKeywords = UrlHelper.getQueryStringParam("q", url);
|
||||
|
||||
if (queryStringKeywords && queryStringKeywords !== _self._searchQuery) {
|
||||
_self._searchQuery = queryStringKeywords;
|
||||
_self.context.dynamicDataSourceManager.notifyPropertyChanged('queryStringQuery');
|
||||
}
|
||||
_self._updateQuery(_self, url);
|
||||
|
||||
// Call the original function with the provided arguments
|
||||
// This context is necessary for the context of the history change
|
||||
return _defaultPushState.apply(this, [data, title, url]);
|
||||
};
|
||||
};
|
||||
}).bind(this);
|
||||
|
||||
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 {
|
||||
RelevantResults: ISearchResult[];
|
||||
RefinementResults: IRefinementResult[];
|
||||
PromotedResults?: IPromotedResult[];
|
||||
TotalRows?: number;
|
||||
}
|
||||
|
||||
|
@ -14,6 +15,12 @@ export interface IRefinementResult {
|
|||
Values: IRefinementValue[];
|
||||
}
|
||||
|
||||
export interface IPromotedResult {
|
||||
Url: string;
|
||||
Title: string;
|
||||
Description: string;
|
||||
}
|
||||
|
||||
export interface IRefinementValue {
|
||||
RefinementCount: number;
|
||||
RefinementName: string;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import ISearchService from './ISearchService';
|
||||
import { ISearchResults, IRefinementFilter, ISearchResult } from '../../models/ISearchResult';
|
||||
import intersection from 'lodash-es/intersection';
|
||||
import clone from 'lodash-es/clone';
|
||||
import { intersection, clone } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
class MockSearchService implements ISearchService {
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as Handlebars from 'handlebars';
|
||||
import ISearchService from './ISearchService';
|
||||
import { ISearchResults, ISearchResult, IRefinementResult, IRefinementValue, IRefinementFilter } from '../../models/ISearchResult';
|
||||
import { sp, SearchQuery, SearchResults, SPRest, Web, Sort, SortDirection, SearchSuggestQuery } from '@pnp/sp';
|
||||
import { ISearchResults, ISearchResult, IRefinementResult, IRefinementValue, IRefinementFilter, IPromotedResult } from '../../models/ISearchResult';
|
||||
import { sp, SearchQuery, SearchResults, SPRest, Sort, SortDirection, SearchSuggestQuery } from '@pnp/sp';
|
||||
import { Logger, LogLevel, ConsoleListener } from '@pnp/logging';
|
||||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import sortBy from 'lodash-es/sortBy';
|
||||
import groupBy from 'lodash-es/groupBy';
|
||||
import mapValues from 'lodash-es/mapValues';
|
||||
import mapKeys from 'lodash-es/mapKeys';
|
||||
import {sortBy, groupBy} from '@microsoft/sp-lodash-subset';
|
||||
const mapKeys: any = require('lodash/mapKeys');
|
||||
const mapValues: any = require('lodash/mapValues');
|
||||
import LocalizationHelper from '../../helpers/LocalizationHelper';
|
||||
|
||||
declare var System: any;
|
||||
|
||||
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
|
||||
const relevantResults: ISearchResult[] = await Promise.all(allItemsPromises);
|
||||
|
||||
|
@ -283,18 +307,17 @@ class SearchService implements ISearchService {
|
|||
private async _mapToIcon(filename: string): Promise<string> {
|
||||
|
||||
const webAbsoluteUrl = this._context.pageContext.web.absoluteUrl;
|
||||
const web = new Web(webAbsoluteUrl);
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
return iconUrl;
|
||||
|
||||
} catch (error) {
|
||||
Logger.write('[SharePointDataProvider._mapToIcon()]: Error: ' + error, LogLevel.Error);
|
||||
throw error;
|
||||
Logger.write('[SearchService._mapToIcon()]: Error: ' + error, LogLevel.Error);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,12 +350,13 @@ class SearchService implements ISearchService {
|
|||
let refinementQueryConditions: string[] = [];
|
||||
let refinementQueryString: string = null;
|
||||
|
||||
// Conditions between values inside a refiner property
|
||||
const refinementFilters = mapValues(groupBy(selectedFilters, 'FilterName'), (values) => {
|
||||
const refinementFilter = values.map((filter) => {
|
||||
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) => {
|
||||
|
@ -357,6 +381,7 @@ class SearchService implements ISearchService {
|
|||
|
||||
// Multiple filters
|
||||
case (conditionsCount > 1): {
|
||||
// Conditions between refiner properties
|
||||
refinementQueryString = Text.format('and({0})', refinementQueryConditions.toString());
|
||||
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 * as strings from 'SearchWebPartStrings';
|
||||
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;
|
||||
|
||||
abstract class BaseTemplateService {
|
||||
|
@ -35,21 +39,134 @@ abstract class BaseTemplateService {
|
|||
*/
|
||||
public static getListDefaultTemplate(): string {
|
||||
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">
|
||||
{{#if showResultsCount}}
|
||||
<div class="template_resultCount">
|
||||
<label class="ms-fontWeight-semibold">{{getCountMessage totalRows keywords}}</label>
|
||||
</div>
|
||||
{{/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">
|
||||
{{#each items as |item|}}
|
||||
<li class="ms-ListItem ms-ListItem--image" tabindex="0">
|
||||
<div class="ms-ListItem-image template_icon" style="background-image:url('{{iconSrc}}')">
|
||||
<li class="template_listItem" tabindex="0">
|
||||
<div class="template_result">
|
||||
<img class="template_icon" src="{{iconSrc}}"/>
|
||||
<div class="template_contentContainer">
|
||||
<span class=""><a href="{{getUrl item}}">{{Title}}</a></span>
|
||||
<span class="">{{getSummary HitHighlightedSummary}}</span>
|
||||
<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>
|
||||
<span class="ms-ListItem-primaryText"><a href="{{getUrl item}}">{{Title}}</a></span>
|
||||
<span class="ms-ListItem-secondaryText">{{getSummary HitHighlightedSummary}}</span>
|
||||
<span class="ms-ListItem-tertiaryText">{{getDate Created "LL"}}</span>
|
||||
<div class="ms-ListItem-selectionTarget"></div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
@ -207,9 +324,165 @@ abstract class BaseTemplateService {
|
|||
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 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;
|
|
@ -1,4 +1,5 @@
|
|||
import { PageOpenBehavior } from '../../helpers/UrlHelper';
|
||||
import IDynamicDataSourceConnection from '../../models/IDynamicDataSourceConnection';
|
||||
|
||||
interface ISearchBoxWebPartProps {
|
||||
searchInNewPage: boolean;
|
||||
|
@ -6,9 +7,7 @@ interface ISearchBoxWebPartProps {
|
|||
openBehavior: PageOpenBehavior;
|
||||
enableQuerySuggestions: boolean;
|
||||
useDynamicDataSource: boolean;
|
||||
dynamicDataSourceId: string;
|
||||
dynamicDataSourcePropertyId: string;
|
||||
dynamicDataSourceComponentId: string;
|
||||
sourceInstance: IDynamicDataSourceConnection;
|
||||
}
|
||||
|
||||
export default ISearchBoxWebPartProps;
|
|
@ -20,15 +20,16 @@ import { Log, Text, Environment, EnvironmentType } from
|
|||
import ISearchService from '../../services/SearchService/ISearchService';
|
||||
import MockSearchService from '../../services/SearchService/MockSearchService';
|
||||
import SearchService from '../../services/SearchService/SearchService';
|
||||
|
||||
const LOG_SOURCE: string = '[SearchBoxWebPart_{0}]';
|
||||
import DynamicDataHelper from '../../helpers/DynamicDataHelper';
|
||||
|
||||
export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWebPartProps> implements IDynamicDataCallables {
|
||||
|
||||
private readonly LOG_SOURCE: string = '[SearchBoxWebPart_{0}]';
|
||||
|
||||
private _searchService: ISearchService;
|
||||
private _searchQuery: string;
|
||||
private _source: IDynamicDataSource;
|
||||
private _domElement: HTMLElement;
|
||||
private _dynamicDataHelper: DynamicDataHelper;
|
||||
|
||||
/**
|
||||
* Used to be able to unregister dynamic data events if the source is updated
|
||||
|
@ -50,38 +51,31 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
}
|
||||
|
||||
/**
|
||||
* Resolves the connected data sources
|
||||
* 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
|
||||
* Binds data source properties to the Web Part properties. In some cases, the data source configuration is not retrieved propertly due to updated ids
|
||||
*/
|
||||
private _initDynamicDataSource() {
|
||||
private _bindDataSources() {
|
||||
|
||||
if (this.properties.dynamicDataSourceId
|
||||
&& this.properties.dynamicDataSourcePropertyId
|
||||
&& this.properties.dynamicDataSourceComponentId) {
|
||||
const sourceFound = this._source ? true : false;
|
||||
|
||||
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId && !sourceFound) {
|
||||
|
||||
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSourceId);
|
||||
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 {
|
||||
// Try to resolve the source and get its id by the name
|
||||
this._source = this._tryGetSourceByComponentId(this.properties.dynamicDataSourceComponentId);
|
||||
this._source = this._dynamicDataHelper._tryGetSourceByInstanceOrComponentId(this.properties.sourceInstance);
|
||||
sourceId = this._source ? this._source.id : undefined;
|
||||
}
|
||||
|
||||
if (sourceId) {
|
||||
this.context.dynamicDataProvider.registerPropertyChanged(sourceId, this.properties.dynamicDataSourcePropertyId, this._dataSourceUpdated);
|
||||
this._searchQuery = this._source.getPropertyValue(this.properties.dynamicDataSourcePropertyId);
|
||||
this.context.dynamicDataProvider.registerPropertyChanged(sourceId, this.properties.sourceInstance.propertyId, this.render);
|
||||
|
||||
// Update the property for the property pane
|
||||
this.properties.dynamicDataSourceId = sourceId;
|
||||
this._lastSourceId = this.properties.dynamicDataSourceId;
|
||||
this._lastPropertyId = this.properties.dynamicDataSourcePropertyId;
|
||||
|
||||
// Notify subscriber of the initial value
|
||||
this.context.dynamicDataSourceManager.notifyPropertyChanged('inputQuery');
|
||||
this.properties.sourceInstance.sourceId = sourceId;
|
||||
this._lastSourceId = sourceId;
|
||||
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 (this.renderedOnce) {
|
||||
|
@ -110,17 +104,21 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
return {
|
||||
key: source.id,
|
||||
text: source.metadata.title,
|
||||
instanceId: source.metadata.instanceId,
|
||||
componentId: source.metadata.componentId
|
||||
};
|
||||
}).filter((item) => {
|
||||
if (item.key.localeCompare("PageContext") !== 0 && item.componentId !== this.componentId) {
|
||||
// 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.dynamicDataSourceId;
|
||||
|
||||
const selectedSource: string = this.properties.sourceInstance.sourceId;
|
||||
let propertyOptions: IPropertyPaneDropdownOption[] = [];
|
||||
|
||||
if (selectedSource) {
|
||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
||||
if (source) {
|
||||
|
@ -134,17 +132,17 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
}
|
||||
|
||||
searchQueryConfigFields = searchQueryConfigFields.concat([
|
||||
PropertyPaneDropdown('dynamicDataSourceId', {
|
||||
label: "Source",
|
||||
PropertyPaneDropdown('sourceInstance.sourceId', {
|
||||
label: strings.DynamicDataSourceLabel,
|
||||
options: sourceOptions,
|
||||
selectedKey: this.properties.dynamicDataSourceId,
|
||||
selectedKey: this.properties.sourceInstance.sourceId,
|
||||
}),
|
||||
PropertyPaneDropdown('dynamicDataSourcePropertyId', {
|
||||
disabled: !this.properties.dynamicDataSourceId,
|
||||
label: "Source property",
|
||||
PropertyPaneDropdown('sourceInstance.propertyId', {
|
||||
disabled: !this.properties.sourceInstance.sourceId,
|
||||
label: strings.DynamicDataSourcePropertyLabel,
|
||||
options: propertyOptions,
|
||||
selectedKey: this.properties.dynamicDataSourcePropertyId
|
||||
}),
|
||||
selectedKey: this.properties.sourceInstance.propertyId
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -188,21 +186,6 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
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
|
||||
* @param value the URL to verify
|
||||
|
@ -216,28 +199,6 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
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
|
||||
*/
|
||||
|
@ -253,51 +214,63 @@ 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> {
|
||||
|
||||
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.context.dynamicDataSourceManager.initializeSource(this);
|
||||
|
||||
// Make sure the data source will be plugged in correctly when loaded on the page
|
||||
// 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));
|
||||
// Re bind data sources to WebPart properties
|
||||
this._reconnectDataSource();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(changedProperty: string) {
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string) {
|
||||
|
||||
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.context.dynamicDataProvider.tryGetSource(this.properties.sourceInstance.sourceId).getPropertyDefinitions()[0].id;
|
||||
}
|
||||
|
||||
this.properties.dynamicDataSourcePropertyId = this._source.getPropertyDefinitions()[0].id;
|
||||
this.properties.dynamicDataSourceComponentId = this._source.metadata.componentId;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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.useDynamicDataSource) {
|
||||
this.context.dynamicDataProvider.unregisterAvailableSourcesChanged(this._initDynamicDataSource.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
|
@ -341,6 +314,22 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
|
||||
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(
|
||||
SearchBox, {
|
||||
onSearch: this._onSearch,
|
||||
|
@ -352,6 +341,6 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
searchService: this._searchService
|
||||
} 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 { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import * as update from 'immutability-helper';
|
||||
import '../SearchBoxWebPart.scss';
|
||||
import styles from '../SearchBoxWebPart.module.scss';
|
||||
|
||||
const SUGGESTION_CHAR_COUNT_TRIGGER = 3;
|
||||
|
||||
|
@ -125,9 +125,9 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
|||
let suggestions: JSX.Element[] = null;
|
||||
|
||||
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 className="suggestionItem">
|
||||
<div className={styles.suggestionItem}>
|
||||
<Spinner size={ SpinnerSize.small }/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -142,13 +142,13 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
|||
style={{
|
||||
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>
|
||||
</Label>
|
||||
</div>;
|
||||
});
|
||||
|
||||
renderSuggestions = <div className="suggestionPanel">
|
||||
renderSuggestions = <div className={styles.suggestionPanel}>
|
||||
{ suggestions }
|
||||
</div>;
|
||||
}
|
||||
|
@ -285,7 +285,7 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
|||
errorMessage: null,
|
||||
});
|
||||
}}
|
||||
className="errorMessage">
|
||||
className={styles.errorMessage}>
|
||||
{ this.state.errorMessage }</MessageBar>;
|
||||
}
|
||||
|
||||
|
@ -293,7 +293,7 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
|||
this.renderSearchBoxWithAutoComplete() :
|
||||
this.renderBasicSearchBox();
|
||||
return (
|
||||
<div className="searchBox">
|
||||
<div className={styles.searchBox}>
|
||||
{ renderErrorMessage }
|
||||
{ renderSearchBox }
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,8 @@ define([], function() {
|
|||
"SearchBoxPageOpenBehaviorLabel": "Opening behavior",
|
||||
"SearchBoxDynamicPropertyInputLabel": "Input value",
|
||||
"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",
|
||||
"SearchBoxDynamicPropertyInputLabel": "Valeur du champ 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;
|
||||
UseDynamicDataSourceLabel: string;
|
||||
SearchBoxDynamicDataSourceGroupName: string;
|
||||
DynamicDataSourceLabel: string;
|
||||
DynamicDataSourcePropertyLabel: string;
|
||||
}
|
||||
|
||||
declare module 'SearchBoxWebPartStrings' {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ResultsLayoutOption from '../../models/ResultsLayoutOption';
|
||||
import IDynamicDataSourceConnection from '../../models/IDynamicDataSourceConnection';
|
||||
|
||||
export interface ISearchResultsWebPartProps {
|
||||
queryKeywords: string;
|
||||
|
@ -16,9 +17,7 @@ export interface ISearchResultsWebPartProps {
|
|||
selectedLayout: ResultsLayoutOption;
|
||||
externalTemplateUrl: string;
|
||||
inlineTemplateText: string;
|
||||
dynamicDataSourceId: string;
|
||||
dynamicDataSourcePropertyId: string;
|
||||
dynamicDataSourceComponentId: string;
|
||||
useHandlebarsHelpers: boolean;
|
||||
webPartTitle: string;
|
||||
sourceInstance: IDynamicDataSourceConnection;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"queryKeywords": "",
|
||||
"queryTemplate": "{searchTerms} Path:{Site}",
|
||||
"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,
|
||||
"maxResultsCount": 10,
|
||||
"showBlank": true,
|
||||
|
|
|
@ -37,21 +37,22 @@ import { update, isEmpty } from '@microsoft/sp-lodash-subset';
|
|||
import MockTemplateService from '../../services/TemplateService/MockTemplateService';
|
||||
import BaseTemplateService from '../../services/TemplateService/BaseTemplateService';
|
||||
import { IDynamicDataSource } from '@microsoft/sp-dynamic-data';
|
||||
import DynamicDataHelper from '../../helpers/DynamicDataHelper';
|
||||
|
||||
declare var System: any;
|
||||
|
||||
const LOG_SOURCE: string = '[SearchResultsWebPart_{0}]';
|
||||
|
||||
export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchResultsWebPartProps> {
|
||||
|
||||
private readonly LOG_SOURCE: string = '[SearchResultsWebPart_{0}]';
|
||||
|
||||
private _searchService: ISearchService;
|
||||
private _taxonomyService: ITaxonomyService;
|
||||
private _templateService: BaseTemplateService;
|
||||
private _useResultSource: boolean;
|
||||
private _queryKeywords: string;
|
||||
private _source: IDynamicDataSource;
|
||||
private _domElement: HTMLElement;
|
||||
private _propertyPage = null;
|
||||
private _dynamicDataHelper: DynamicDataHelper;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
* Binds data source properties to the Web Part properties. In some cases, the data source configuration is not retrieved propertly due to updated ids
|
||||
*/
|
||||
private _initDynamicDataSource() {
|
||||
private _bindDataSources() {
|
||||
|
||||
if (this.properties.dynamicDataSourceId
|
||||
&& this.properties.dynamicDataSourcePropertyId
|
||||
&& this.properties.dynamicDataSourceComponentId) {
|
||||
const sourceFound = this._source ? true : false;
|
||||
|
||||
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId && !sourceFound) {
|
||||
|
||||
this._source = this.context.dynamicDataProvider.tryGetSource(this.properties.dynamicDataSourceId);
|
||||
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 {
|
||||
// Try to resolve the source and get its id by the name
|
||||
this._source = this._tryGetSourceByComponentId(this.properties.dynamicDataSourceComponentId);
|
||||
this._source = this._dynamicDataHelper._tryGetSourceByInstanceOrComponentId(this.properties.sourceInstance);
|
||||
sourceId = this._source ? this._source.id : undefined;
|
||||
}
|
||||
|
||||
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
|
||||
this.properties.dynamicDataSourceId = sourceId;
|
||||
this._lastSourceId = this.properties.dynamicDataSourceId;
|
||||
this._lastPropertyId = this.properties.dynamicDataSourcePropertyId;
|
||||
this.properties.sourceInstance.sourceId = sourceId;
|
||||
this._lastSourceId = sourceId;
|
||||
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 (this.renderedOnce) {
|
||||
|
@ -109,26 +107,15 @@ 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
|
||||
* @param dataSourceComponentId the component id
|
||||
* 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 _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;
|
||||
private _reconnectDataSource() {
|
||||
if (this.properties.sourceInstance.sourceId && this.properties.sourceInstance.propertyId) {
|
||||
this.context.dynamicDataProvider.registerAvailableSourcesChanged(this._bindDataSources.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
|
@ -178,7 +165,15 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
resizable: true,
|
||||
value: this.properties.refiners,
|
||||
deferredValidationTime: 300,
|
||||
})
|
||||
}),
|
||||
PropertyPaneSlider('maxResultsCount', {
|
||||
label: strings.MaxResultsCount,
|
||||
max: 50,
|
||||
min: 1,
|
||||
showValue: true,
|
||||
step: 1,
|
||||
value: 50,
|
||||
}),
|
||||
];
|
||||
|
||||
return searchSettingsFields;
|
||||
|
@ -201,13 +196,22 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
this.context.dynamicDataProvider.getAvailableSources().map(source => {
|
||||
return {
|
||||
key: source.id,
|
||||
text: source.metadata.title
|
||||
text: source.metadata.title,
|
||||
instanceId: source.metadata.instanceId,
|
||||
componentId: source.metadata.componentId
|
||||
};
|
||||
}).filter(item => item.key.localeCompare("PageContext") !== 0);
|
||||
|
||||
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[] = [];
|
||||
|
||||
if (selectedSource) {
|
||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
||||
if (source) {
|
||||
|
@ -221,17 +225,17 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
}
|
||||
|
||||
searchQueryConfigFields = searchQueryConfigFields.concat([
|
||||
PropertyPaneDropdown('dynamicDataSourceId', {
|
||||
label: "Source",
|
||||
PropertyPaneDropdown('sourceInstance.sourceId', {
|
||||
label: strings.DynamicDataSourceLabel,
|
||||
options: sourceOptions,
|
||||
selectedKey: this.properties.dynamicDataSourceId,
|
||||
selectedKey: this.properties.sourceInstance.sourceId,
|
||||
}),
|
||||
PropertyPaneDropdown('dynamicDataSourcePropertyId', {
|
||||
disabled: !this.properties.dynamicDataSourceId,
|
||||
label: "Source property",
|
||||
PropertyPaneDropdown('sourceInstance.propertyId', {
|
||||
disabled: !this.properties.sourceInstance.sourceId,
|
||||
label: strings.DynamicDataSourcePropertyLabel,
|
||||
options: propertyOptions,
|
||||
selectedKey: this.properties.dynamicDataSourcePropertyId
|
||||
}),
|
||||
selectedKey: this.properties.sourceInstance.propertyId
|
||||
})
|
||||
]);
|
||||
|
||||
} 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;
|
||||
}
|
||||
|
@ -514,7 +507,16 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
*/
|
||||
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) {
|
||||
this._searchService = new MockSearchService();
|
||||
|
@ -537,9 +539,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
// Set the default search results layout
|
||||
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
|
||||
// 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));
|
||||
this._reconnectDataSource();
|
||||
|
||||
return super.onInit();
|
||||
}
|
||||
|
@ -564,8 +564,8 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
const searchContainer: React.ReactElement<ISearchContainerProps> = React.createElement(
|
||||
SearchContainer,
|
||||
{
|
||||
searchDataProvider: this._searchService,
|
||||
taxonomyDataProvider: this._taxonomyService,
|
||||
searchService: this._searchService,
|
||||
taxonomyService: this._taxonomyService,
|
||||
queryKeywords: this._queryKeywords,
|
||||
maxResultsCount: this.properties.maxResultsCount,
|
||||
resultSourceId: this.properties.resultSourceId,
|
||||
|
@ -596,7 +596,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
);
|
||||
|
||||
if ((this.properties.queryKeywords && !this.properties.useSearchBoxQuery) ||
|
||||
(this.properties.useSearchBoxQuery && this.properties.dynamicDataSourcePropertyId)) {
|
||||
(this.properties.useSearchBoxQuery && this.properties.sourceInstance.sourceId)) {
|
||||
renderElement = searchContainer;
|
||||
} else {
|
||||
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 {
|
||||
|
@ -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) {
|
||||
|
||||
// Reset source settings if we don't use search query
|
||||
this.properties.dynamicDataSourceId = undefined;
|
||||
this.properties.dynamicDataSourcePropertyId = undefined;
|
||||
this.context.dynamicDataProvider.unregisterAvailableSourcesChanged(this._initDynamicDataSource.bind(this));
|
||||
this.properties.sourceInstance.sourceId = undefined;
|
||||
this.properties.sourceInstance.propertyId = undefined;
|
||||
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...
|
||||
if (changedProperty === 'inlineTemplateText') {
|
||||
if (propertyPath === 'inlineTemplateText') {
|
||||
|
||||
// Automatically switch the option to 'Custom' if a default template has been edited
|
||||
// (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);
|
||||
}
|
||||
|
||||
|
@ -716,8 +728,18 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
|
||||
// If a source is selected, use the value from here
|
||||
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 { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
||||
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
|
||||
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||
import * as strings from 'SearchWebPartStrings';
|
||||
import { IRefinementValue, IRefinementFilter } from '../../../../models/ISearchResult';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import '../SearchResultsWebPart.scss';
|
||||
import * as update from 'immutability-helper';
|
||||
import {
|
||||
GroupedList,
|
||||
|
@ -16,7 +14,8 @@ import {
|
|||
IGroupDividerProps
|
||||
} from 'office-ui-fabric-react/lib/components/GroupedList/index';
|
||||
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> {
|
||||
|
||||
|
@ -34,7 +33,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
this._addFilter = this._addFilter.bind(this);
|
||||
this._removeFilter = this._removeFilter.bind(this);
|
||||
this._isInFilterSelection = this._isInFilterSelection.bind(this);
|
||||
this._applyAllfilters = this._applyAllfilters.bind(this);
|
||||
this._removeAllFilters = this._removeAllFilters.bind(this);
|
||||
this._onRenderHeader = this._onRenderHeader.bind(this);
|
||||
this._onRenderCell = this._onRenderCell.bind(this);
|
||||
|
@ -61,7 +59,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
|
||||
items.push(
|
||||
<div key={i}>
|
||||
<div className='filterPanel__filterProperty'>
|
||||
{
|
||||
filter.Values.map((refinementValue: IRefinementValue, j) => {
|
||||
|
||||
|
@ -85,14 +82,13 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const renderSelectedFilters: JSX.Element[] = this.state.selectedFilters.map((filter) => {
|
||||
|
||||
return (
|
||||
<Label className='filter'>
|
||||
<Label className={styles.filter}>
|
||||
<i className='ms-Icon ms-Icon--ClearFilter' onClick={() => { this._removeFilter(filter); }}></i>
|
||||
{filter.Value.RefinementName}
|
||||
</Label>
|
||||
|
@ -103,7 +99,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
ref='groupedList'
|
||||
items={items}
|
||||
onRenderCell={this._onRenderCell}
|
||||
className='filterPanel__body__group'
|
||||
className={styles.searchWp__filterPanel__body__group}
|
||||
groupProps={
|
||||
{
|
||||
onRenderHeader: this._onRenderHeader,
|
||||
|
@ -111,46 +107,47 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
}
|
||||
groups={groups} />;
|
||||
|
||||
const renderLinkRemoveAll = this.state.selectedFilters.length > 0 ?
|
||||
<Link onClick={this._removeAllFilters}>
|
||||
{strings.RemoveAllFiltersLabel}
|
||||
</Link> : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ms-textAlignRight">
|
||||
<ActionButton
|
||||
className='searchWp__filterResultBtn'
|
||||
className={`${styles.searchWp__filterResultBtn} ms-fontWeight-semibold`}
|
||||
iconProps={{ iconName: 'Filter' }}
|
||||
text={strings.FilterResultsButtonLabel}
|
||||
onClick={this._onTogglePanel}
|
||||
/>
|
||||
</div>
|
||||
{(this.state.selectedFilters.length > 0) ?
|
||||
|
||||
<div className='searchWp__selectedFilters'>
|
||||
<div className={styles.searchWp__selectedFilters}>
|
||||
{renderSelectedFilters}
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<Panel
|
||||
className='filterPanel'
|
||||
className={styles.searchWp__filterPanel}
|
||||
isOpen={this.state.showPanel}
|
||||
type={PanelType.smallFixedNear}
|
||||
type={PanelType.custom}
|
||||
customWidth="450px"
|
||||
isBlocking={false}
|
||||
isLightDismiss={true}
|
||||
onDismiss={this._onClosePanel}
|
||||
headerText={strings.FilterPanelTitle}
|
||||
closeButtonAriaLabel='Close'
|
||||
hasCloseButton={true}
|
||||
headerClassName='filterPanel__header'
|
||||
onRenderBody={() => {
|
||||
if (this.props.availableFilters.length > 0) {
|
||||
return (
|
||||
<Scrollbars style={{ height: '100%' }}>
|
||||
<div className='filterPanel__body'>
|
||||
<div className='filterPanel__body__allFiltersToggle'>
|
||||
<Toggle
|
||||
onText={strings.RemoveAllFiltersLabel}
|
||||
offText={strings.ApplyAllFiltersLabel}
|
||||
onChanged={(checked: boolean) => {
|
||||
checked ? this._applyAllfilters() : this._removeAllFilters();
|
||||
}}
|
||||
checked={this.state.selectedFilters.length === 0 ? false : true}
|
||||
/>
|
||||
<Scrollbars style={{height: '100%'}}>
|
||||
<div className={styles.searchWp__filterPanel__body}>
|
||||
<div
|
||||
className={`${styles.searchWp__filterPanel__body__allFiltersToggle} ${this.state.selectedFilters.length == 0 && "hiddenLink"}`}>
|
||||
{renderLinkRemoveAll}
|
||||
</div>
|
||||
{renderAvailableFilters}
|
||||
</div>
|
||||
|
@ -158,7 +155,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='filterPanel__body'>
|
||||
<div className={styles.searchWp__filterPanel__body}>
|
||||
{strings.NoFilterConfiguredLabel}
|
||||
</div>
|
||||
);
|
||||
|
@ -169,6 +166,22 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className='ms-Grid-row' data-selection-index={itemIndex}>
|
||||
|
@ -181,7 +194,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
|
||||
private _onRenderHeader(props: IGroupDividerProps): JSX.Element {
|
||||
return (
|
||||
|
||||
<div className={styles.searchWp__filterPanel__body__group__header}>
|
||||
<div className='ms-Grid-row' onClick={() => {
|
||||
|
||||
// Update the index for expanded groups to be able to keep it open after a re-render
|
||||
|
@ -197,7 +210,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
props.onToggleCollapse(props.group);
|
||||
}}>
|
||||
<div className='ms-Grid-col ms-u-sm1 ms-u-md1 ms-u-lg1'>
|
||||
<div className='header-icon'>
|
||||
<div className={styles.searchWp__filterPanel__body__headerIcon}>
|
||||
<i className={props.group.isCollapsed ? 'ms-Icon ms-Icon--ChevronDown' : 'ms-Icon ms-Icon--ChevronUp'}></i>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -205,6 +218,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
<div className='ms-font-l'>{props.group.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -219,7 +233,7 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
private _addFilter(filterToAdd: IRefinementFilter): void {
|
||||
|
||||
// Add the filter to the selected filters collection
|
||||
let newFilters = update(this.state.selectedFilters, { $push: [filterToAdd] });
|
||||
let newFilters = update(this.state.selectedFilters, {$push: [filterToAdd]});
|
||||
this._applyFilters(newFilters);
|
||||
}
|
||||
|
||||
|
@ -233,20 +247,6 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
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 {
|
||||
this._applyFilters([]);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ interface IFilterPanelProps {
|
|||
availableFilters: IRefinementResult[];
|
||||
refinersConfiguration: { [key: string]: string };
|
||||
onUpdateFilters: RefinementFilterOperationCallback;
|
||||
resetSelectedFilters: boolean;
|
||||
}
|
||||
|
||||
export default IFilterPanelProps;
|
|
@ -1,10 +1,12 @@
|
|||
import { ISearchResult } from '../../../../models/ISearchResult';
|
||||
import { ISearchResult, IPromotedResult } from '../../../../models/ISearchResult';
|
||||
|
||||
/**
|
||||
* Handlebars template context for search results
|
||||
*/
|
||||
interface ISearchResultsTemplateContext {
|
||||
items: ISearchResult[];
|
||||
promotedResults?: IPromotedResult[];
|
||||
strings: ISearchWebPartStrings;
|
||||
totalRows: number;
|
||||
keywords: string;
|
||||
showResultsCount: boolean;
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
interface ISearchResultsTemplateState {
|
||||
|
||||
/**
|
||||
* The handlebar compiled template
|
||||
*/
|
||||
processedTemplate: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.searchWp {
|
||||
|
||||
.template_root {
|
||||
@import '~office-ui-fabric/dist/sass/Fabric.scss';
|
||||
@import '~office-ui-fabric/dist/components/Label/Label.scss';
|
||||
@import '~office-ui-fabric/dist/components/List/List.scss';
|
||||
|
@ -6,14 +7,33 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/FabricCore.scss';
|
||||
|
||||
.template_defaultList {
|
||||
.template_icon {
|
||||
background-position: top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
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 {
|
||||
.singleCard {
|
||||
margin: 10px;
|
||||
|
@ -42,6 +62,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.template_resultCount {
|
||||
padding-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -2,24 +2,46 @@ import React = require('react');
|
|||
import ISearchResultsTemplateProps from './ISearchResultsTemplateProps';
|
||||
import ISearchResultsTemplateState from './ISearchResultsTemplateState';
|
||||
import './SearchResultsTemplate.scss';
|
||||
import { Resize } from 'on-el-resize';
|
||||
import { DomHelper } from '../../../../helpers/DomHelper';
|
||||
|
||||
export default class SearchResultsTemplate extends React.Component<ISearchResultsTemplateProps, ISearchResultsTemplateState> {
|
||||
|
||||
private parentRef: HTMLElement;
|
||||
private resize: Resize;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.resize = new Resize();
|
||||
this.state = {
|
||||
processedTemplate: null
|
||||
};
|
||||
|
||||
this.onComponentResize = this.onComponentResize.bind(this);
|
||||
}
|
||||
|
||||
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() {
|
||||
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) {
|
||||
|
@ -37,4 +59,14 @@ export default class SearchResultsTemplate extends React.Component<ISearchResult
|
|||
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 IPagingProps from './IPagingProps';
|
||||
import Pagination from 'react-js-pagination';
|
||||
import styles from '../SearchResultsWebPart.module.scss';
|
||||
|
||||
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> {
|
||||
|
||||
return(
|
||||
<div className='searchWp__paginationContainer'>
|
||||
<div className='searchWp__paginationContainer__pagination'>
|
||||
<div className={styles.searchWp__paginationContainer}>
|
||||
<div className={styles.searchWp__paginationContainer__pagination}>
|
||||
<Pagination
|
||||
activePage={this.props.currentPage}
|
||||
firstPageText={<i className='ms-Icon ms-Icon--DoubleChevronLeft' 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>}
|
||||
nextPageText={<i className='ms-Icon ms-Icon--ChevronRight' aria-hidden='true'></i>}
|
||||
activeLinkClass={ 'active' }
|
||||
activeLinkClass={ styles.active }
|
||||
itemsCountPerPage={ this.props.itemsCountPerPage }
|
||||
totalItemsCount={ this.props.totalItems }
|
||||
pageRangeDisplayed={5}
|
||||
|
|
|
@ -14,12 +14,12 @@ interface ISearchResultsContainerProps {
|
|||
/**
|
||||
* The search data provider instance
|
||||
*/
|
||||
searchDataProvider: ISearchService;
|
||||
searchService: ISearchService;
|
||||
|
||||
/**
|
||||
* The taxonomy data provider instance
|
||||
*/
|
||||
taxonomyDataProvider: ITaxonomyService;
|
||||
taxonomyService: ITaxonomyService;
|
||||
|
||||
/**
|
||||
* The search query keywords
|
||||
|
|
|
@ -42,11 +42,6 @@ interface ISearchResultsContainerState {
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -3,16 +3,16 @@ import ISearchContainerProps from './ISearchResultsContainerProps';
|
|||
import ISearchContainerState from './ISearchResultsContainerState';
|
||||
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
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 * as strings from 'SearchWebPartStrings';
|
||||
import { IRefinementFilter, IRefinementValue, IRefinementResult } from '../../../../models/ISearchResult';
|
||||
import '../SearchResultsWebPart.scss';
|
||||
import Paging from '../Paging/Paging';
|
||||
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import SearchResultsTemplate from '../Layouts/SearchResultsTemplate';
|
||||
import styles from '../SearchResultsWebPart.module.scss';
|
||||
|
||||
declare var System: any;
|
||||
let FilterPanel = null;
|
||||
|
@ -33,7 +33,6 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
availableFilters: [],
|
||||
currentPage: 1,
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: true,
|
||||
errorMessage: '',
|
||||
hasError: false,
|
||||
lastQuery: ''
|
||||
|
@ -49,58 +48,75 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
const items = this.state.results;
|
||||
const hasError = this.state.hasError;
|
||||
const errorMessage = this.state.errorMessage;
|
||||
const isComponentLoading = this.state.isComponentLoading;
|
||||
|
||||
let renderWpContent: JSX.Element = null;
|
||||
let renderOverlay: JSX.Element = null;
|
||||
let renderWebPartTitle: JSX.Element = null;
|
||||
|
||||
if (!isComponentLoading && areResultsLoading) {
|
||||
if (areResultsLoading) {
|
||||
|
||||
if (items.RelevantResults.length > 0) {
|
||||
renderOverlay = <div>
|
||||
<Overlay isDarkThemed={false} className='overlay'>
|
||||
<Overlay isDarkThemed={false} className={styles.overlay}>
|
||||
<Spinner size={SpinnerSize.medium} />
|
||||
</Overlay>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let webPartTitle = null;
|
||||
if (this.props.webPartTitle && this.props.webPartTitle.length > 0) {
|
||||
webPartTitle = <WebPartTitle title={this.props.webPartTitle} updateProperty={null} displayMode={DisplayMode.Read} />;
|
||||
}
|
||||
|
||||
if (isComponentLoading) {
|
||||
//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 {
|
||||
let i = 0;
|
||||
let renderShimmerElements: JSX.Element[] = [];
|
||||
while (i < 4) {
|
||||
renderShimmerElements.push(
|
||||
<Shimmer
|
||||
customElementsGroup={this._getShimmerElements()}
|
||||
width="100%"
|
||||
style={{ marginBottom: "20px" }}
|
||||
/>);
|
||||
i++;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
let filterPanel = this.state.availableFilters && this.state.availableFilters.length > 0 ? <FilterPanel availableFilters={this.state.availableFilters} onUpdateFilters={this._onUpdateFilters} refinersConfiguration={this.props.refiners} /> : <span />;
|
||||
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) {
|
||||
|
||||
if (!this.props.showBlank) {
|
||||
// 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>
|
||||
{webPartTitle}
|
||||
{filterPanel}
|
||||
<div className='searchWp__noresult'>{strings.NoResultMessage}</div>
|
||||
{renderWebPartTitle}
|
||||
{renderFilterPanel}
|
||||
<div className={styles.searchWp__noresult}>{strings.NoResultMessage}</div>
|
||||
</div>;
|
||||
} else {
|
||||
if (this.props.displayMode === DisplayMode.Edit) {
|
||||
if (this.props.displayMode === DisplayMode.Edit && !areResultsLoading) {
|
||||
renderWpContent = <MessageBar messageBarType={MessageBarType.info}>{strings.ShowBlankEditInfoMessage}</MessageBar>;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
renderWpContent =
|
||||
<div>
|
||||
{webPartTitle}
|
||||
{filterPanel}
|
||||
{renderWebPartTitle}
|
||||
{renderFilterPanel}
|
||||
{renderOverlay}
|
||||
<SearchResultsTemplate
|
||||
templateService={this.props.templateService}
|
||||
|
@ -108,13 +124,15 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
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
|
||||
actualResultsCount: items.RelevantResults.length,
|
||||
strings: strings,
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
@ -129,10 +147,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='searchWp'>
|
||||
<div className={styles.searchWp}>
|
||||
{renderWpContent}
|
||||
</div>
|
||||
);
|
||||
|
@ -148,11 +165,11 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
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 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);
|
||||
|
||||
if (localizedFilters && localizedFilters.length > 0) {
|
||||
|
@ -163,16 +180,12 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
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({
|
||||
results: searchResults,
|
||||
resultCount: searchResults.TotalRows,
|
||||
availableFilters: localizedFilters,
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
lastQuery: this.props.queryKeywords + this.props.searchDataProvider.queryTemplate + this.props.selectedProperties.join(',')
|
||||
lastQuery: this.props.queryKeywords + this.props.searchService.queryTemplate + this.props.selectedProperties.join(',')
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
@ -181,7 +194,6 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
results: { RefinementResults: [], RelevantResults: [] },
|
||||
hasError: true,
|
||||
errorMessage: error.message
|
||||
|
@ -189,15 +201,14 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
}
|
||||
} else {
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
areResultsLoading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (JSON.stringify(this.props.refiners) !== JSON.stringify(nextProps.refiners)
|
||||
|
@ -216,15 +227,15 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
this.props.searchDataProvider.selectedProperties = nextProps.selectedProperties;
|
||||
this.props.searchService.selectedProperties = nextProps.selectedProperties;
|
||||
|
||||
const refinerManagedProperties = Object.keys(nextProps.refiners).join(',');
|
||||
|
||||
// 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);
|
||||
|
||||
if (FilterPanel == null && localizedFilters && localizedFilters.length > 0) {
|
||||
if (FilterPanel === null && localizedFilters && localizedFilters.length > 0) {
|
||||
const filterPanelComponent = await System.import(
|
||||
/* webpackChunkName: 'search-filterpanel' */
|
||||
'../FilterPanel'
|
||||
|
@ -238,6 +249,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
resultCount: searchResults.TotalRows,
|
||||
availableFilters: localizedFilters,
|
||||
areResultsLoading: false,
|
||||
currentPage: 1,
|
||||
lastQuery: query
|
||||
});
|
||||
|
||||
|
@ -247,7 +259,6 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
results: { RefinementResults: [], RelevantResults: [] },
|
||||
hasError: true,
|
||||
errorMessage: error.message
|
||||
|
@ -255,8 +266,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
}
|
||||
} else {
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
isComponentLoading: false,
|
||||
areResultsLoading: false
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -293,10 +303,14 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
|
||||
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({
|
||||
results: searchResults,
|
||||
availableFilters: localizedFilters,
|
||||
areResultsLoading: false,
|
||||
});
|
||||
}
|
||||
|
@ -314,7 +328,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
|
||||
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({
|
||||
results: searchResults,
|
||||
|
@ -359,8 +373,8 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
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...
|
||||
await this.props.taxonomyDataProvider.initialize();
|
||||
const termValues = await this.props.taxonomyDataProvider.getTermsById(termsToLocalize.map((t) => { return t.termId; }));
|
||||
await this.props.taxonomyService.initialize();
|
||||
const termValues = await this.props.taxonomyService.getTermsById(termsToLocalize.map((t) => { return t.termId; }));
|
||||
|
||||
const termsEnumerator = termValues.getEnumerator();
|
||||
|
||||
|
@ -432,4 +446,24 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
|
||||
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 {
|
||||
|
||||
min-height: 35px;
|
||||
|
||||
// Needed to avoid overlay overflow
|
||||
position: relative;
|
||||
|
||||
&__noresult {
|
||||
padding:10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__filterResultBtn {
|
||||
color: "[theme: themePrimary]";
|
||||
color: "[theme: themePrimary, default: #005a9e]";
|
||||
}
|
||||
|
||||
&__selectedFilters {
|
||||
|
@ -34,7 +34,7 @@
|
|||
}
|
||||
|
||||
i:hover {
|
||||
color: "[theme: themePrimary]";
|
||||
color: "[theme: themePrimary, default: #005a9e]";
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@
|
|||
text-align: center;
|
||||
margin-top: 15px;
|
||||
|
||||
.searchWp__paginationContainer__pagination {
|
||||
&__pagination {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
|
@ -58,40 +58,42 @@
|
|||
|
||||
a {
|
||||
float: left;
|
||||
color: "[theme: themePrimary]";
|
||||
color: "[theme: themePrimary, default: #005a9e]";
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 15px;
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
color: "[theme: themePrimary]";
|
||||
color: "[theme: themePrimary, default: #005a9e]";
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: "[theme: themePrimary, default: #005a9e]";
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.active {
|
||||
background-color: "[theme: themePrimary]";
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__filterPanel {
|
||||
|
||||
.filterPanel {
|
||||
position: relative;
|
||||
|
||||
&__body {
|
||||
padding: 20px;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
overflow: auto;
|
||||
|
||||
.header-icon {
|
||||
&__headerIcon {
|
||||
text-align: right;
|
||||
margin-bottom:8px;
|
||||
margin-top: 5px;
|
||||
|
||||
.ms-Icon {
|
||||
font-size: 16px;
|
||||
|
@ -106,12 +108,13 @@
|
|||
}
|
||||
|
||||
&__allFiltersToggle {
|
||||
margin-bottom: 25px;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&__group {
|
||||
|
||||
.ms-List-page~.ms-List-page {
|
||||
&__header {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
|
@ -120,6 +123,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
|
@ -10,7 +10,7 @@ define([], function() {
|
|||
"NoResultMessage": "There are no results to show",
|
||||
"RefinersFieldLabel": "Refiners",
|
||||
"FilterPanelTitle": "Available filters",
|
||||
"FilterResultsButtonLabel": "Filter results",
|
||||
"FilterResultsButtonLabel": "Filters",
|
||||
"SelectedFiltersLabel": "Selected filters:",
|
||||
"ApplyAllFiltersLabel": "Apply all filters",
|
||||
"RemoveAllFiltersLabel": "Remove all filters",
|
||||
|
@ -50,6 +50,9 @@ define([], function() {
|
|||
"ErrorTemplateExtension": "The template must be a valid .htm or .html file",
|
||||
"ErrorTemplateResolve": "Unable to resolve the specified template. Error details: '{0}'",
|
||||
"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.",
|
||||
"RefinersFieldLabel": "Filtres",
|
||||
"FilterPanelTitle": "Filtres disponibles",
|
||||
"FilterResultsButtonLabel": "Filtrer l'affichage",
|
||||
"FilterResultsButtonLabel": "Filtrer",
|
||||
"SelectedFiltersLabel": "Filtre(s) appliqué(s):",
|
||||
"ApplyAllFiltersLabel": "Appliquer tous les filters",
|
||||
"RemoveAllFiltersLabel": "Supprimer tous les filtres",
|
||||
|
@ -50,6 +50,9 @@ define([], function() {
|
|||
"ErrorTemplateExtension": "Le file modèle doit être un fichier .htm ou .html valide",
|
||||
"ErrorTemplateResolve": "Impossible de résoudre le fichier. Détails: '{0}'",
|
||||
"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;
|
||||
WebPartTitle: string;
|
||||
HandlebarsHelpersDescription: string;
|
||||
DynamicDataSourceLabel: string;
|
||||
DynamicDataSourcePropertyLabel: string;
|
||||
PromotedResultsLabel: string;
|
||||
}
|
||||
|
||||
declare module 'SearchWebPartStrings' {
|
||||
|
|
Loading…
Reference in New Issue