Added support for page query variables and fixes (#661)

* Added support for {Page} query variables
* Bugfix for making sure the property pane shows the right template on Edit after a change.
* Added getUniqueCount
* Bugfix for dynamic loading of video.js
* Updated readme
This commit is contained in:
Mikael Svenson 2018-10-30 10:25:27 +01:00 committed by GitHub
parent 910d377a77
commit 27977d9235
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 41 deletions

View File

@ -27,14 +27,15 @@ react-search-refiners | Franck Cornu (aequos) - [@FranckCornu](http://www.twitte
Version|Date|Comments
-------|----|--------
1.0 | October 17, 2017 | Initial release
1.1 | January 03, 2018 | Improvements and updating to SPFx drop 1.4
1.2 | February 12, 2018 | Added a search box Web Part + Added a "Result Source Id" and "Enable Query Rules" parameters.
1.3 | April 1, 2018 | Added the result count + entered keywords option
1.0 | Oct 17, 2017 | Initial release
1.1 | Jan 03, 2018 | Improvements and updating to SPFx drop 1.4
1.2 | Feb 12, 2018 | Added a search box Web Part + Added a "Result Source Id" and "Enable Query Rules" parameters.
1.3 | Apr1, 2018 | Added the result count + entered keywords option
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>
2.1.0.0 | Oct 14, 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>
2.1.1.0 | Oct 30, 2018 | <ul><li>Bug fix for editing custom template.</li><li>bug fix for dynamic loading of video helper library.</li><li>Added support for Page context query variables.</li><li>Added `getUniqueCount` helper function.</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.**
@ -114,6 +115,24 @@ This Web Part allows you change customize the way you display your search result
<img src="./images/edit_template.png"/>
</p>
#### Query variables
The following out of the box query variables are supported/tested:
* {searchTerms}
* {Site}
* {SiteCollection}
* {URLToken}
* {User}
* {Today}
* {SearchBoxQuery}
* {CurrentDisplayLanguage}
* {CurrentDisplayLCID}
The following custom query variables are supported:
* {Page.&lt;column&gt;} - where column is the internal name of the column.
* When used with taxonomy columns, use `{Page.Column.Label}` or `{Page.Column.TermID}`
#### Best bets
This WP supports SharePoint best bets via SharePoint query rules:
@ -161,6 +180,8 @@ Setting | Description
`{{<search_managed_property_name>}}` | Any valid search managed property returned in the results set. These are typically managed properties set in the *"Selected properties"* setting in the property pane. You don't need to prefix them with `item.` if you are in the "each" loop.
`{{webUrl}}` | The current web relative url. Use `{{../webUrl}}` inside a loop.
`{{siteUrl}}` | The current site relative url. Use `{{../siteUrl}}` inside a loop.
`{{getUniqueCount items "property"}}` | Get the unique count of a property over the result set (or another array)
`{{getUniqueCount array}}` | Get the unique count of objects in an array. Example: [1,1,1,2,2,4] would return `3`.
Also the [Handlebars helpers](https://github.com/helpers/handlebars-helpers) (188 helpers) are also available. You can also define your own in the *BaseTemplateService.ts* file. See [helper-moment](https://github.com/helpers/helper-moment) for date samples using moment.

View File

@ -3,7 +3,7 @@
"solution": {
"name": "PnP - Search Web Parts",
"id": "890affef-33e0-4d72-bd72-36399e02143b",
"version": "2.1.0.0",
"version": "2.1.1.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"features": [

View File

@ -1,12 +1,12 @@
import * as Handlebars from 'handlebars';
import { ISearchResult } from '../../models/ISearchResult';
import { html } from 'common-tags';
import { isEmpty } from '@microsoft/sp-lodash-subset';
import { isEmpty, uniqBy, uniq } 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 'video.js/dist/video-js.css';
import { Logger } from '@pnp/logging';
import templateStyles from './BaseTemplateService.module.scss';
import templateStyles from './BaseTemplateService.module.scss';
import { DomHelper } from '../../helpers/DomHelper';
declare var System: any;
@ -287,7 +287,7 @@ abstract class BaseTemplateService {
return d;
} catch (error) {
return;
}
}
});
// Return the URL or Title part of a URL automatic managed property
@ -299,6 +299,23 @@ abstract class BaseTemplateService {
}
return urlField.substr(separatorPos + 1).trim();
});
// Return the unique count based on an array or property of an object in the array
// <p>{{getUniqueCount items "Title"}}</p>
Handlebars.registerHelper("getUniqueCount", (array: any[], property: string) => {
if (!Array.isArray(array)) return 0;
if (array.length === 0) return 0;
let result;
if (property) {
result = uniqBy(array, property);
}
else {
result = uniq(array);
}
return result.length;
});
}
/**
@ -353,27 +370,27 @@ abstract class BaseTemplateService {
}
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= '';
thumbnailElt.parentElement.style.display = 'none';
containerElt.style.display = '';
} else {
if (url) {
thumbnailElt.parentElement.style.display= 'none';
thumbnailElt.parentElement.style.display = 'none';
const closeBtnId = `${iframeId}_closeBtn`;
const innerPreviewHtml = `
<iframe id="${iframeId}" class="iframePreview" src="${url}" frameborder="0">
@ -386,9 +403,9 @@ abstract class BaseTemplateService {
newEl.innerHTML = previewHtml;
DomHelper.insertAfter(newEl, thumbnailElt.parentElement);
document.getElementById(closeBtnId).addEventListener("click", ((event) => {
thumbnailElt.parentElement.style.display= '';
document.getElementById(previewContainedId).style.display= 'none';
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`);
@ -402,15 +419,15 @@ abstract class BaseTemplateService {
// Load Videos-Js on Demand
// Webpack will create a other bundle loaded on demand just for this library
const videoJs = await import(
const videoJs = await System.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) => {
@ -418,8 +435,8 @@ abstract class BaseTemplateService {
// 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 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`;
@ -437,12 +454,12 @@ abstract class BaseTemplateService {
// Remove exiting instance if there is already a player registered with id
if (player) {
thumbnailElt.parentElement.style.display= 'none';
containerElt.style.display= '';
thumbnailElt.parentElement.style.display = 'none';
containerElt.style.display = '';
} else {
if (url && fileExtension) {
thumbnailElt.parentElement.style.display= 'none';
thumbnailElt.parentElement.style.display = 'none';
const closeBtnId = `${playerId}_closeBtn`;
@ -468,9 +485,9 @@ abstract class BaseTemplateService {
});
document.getElementById(closeBtnId).addEventListener("click", ((ev) => {
thumbnailElt.parentElement.style.display= '';
thumbnailElt.parentElement.style.display = '';
if(!videoPlayer.paused()) {
if (!videoPlayer.paused()) {
videoPlayer.pause();
}
document.getElementById(previewContainedId).style.display = 'none';
@ -482,7 +499,7 @@ abstract class BaseTemplateService {
}
});
}));
}
}
}
export default BaseTemplateService;

View File

@ -22,10 +22,9 @@ import MockSearchService from
import SearchService from '../../services/SearchService/SearchService';
import DynamicDataHelper from '../../helpers/DynamicDataHelper';
const LOG_SOURCE: string = '[SearchBoxWebPart_{0}]';
export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWebPartProps> implements IDynamicDataCallables {
private readonly LOG_SOURCE: string = '[SearchBoxWebPart_{0}]';
private _searchService: ISearchService;
private _searchQuery: string;
private _source: IDynamicDataSource;
@ -325,7 +324,7 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
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);
Log.warn(Text.format(LOG_SOURCE, this.instanceId), `The selected input value from the dynamic data source is not a string. Received (${typeof sourceValue})`, this.context.serviceScope);
}
}
}

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { Text, Log } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
@ -40,11 +41,9 @@ 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;
@ -459,7 +458,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
* @param propertyPath the name of the updated property
* @param newValue the new value for this property
*/
private _onCustomPropertyPaneChange(propertyPath: string, newValue: any): void {
private async _onCustomPropertyPaneChange(propertyPath: string, newValue: any): Promise<void> {
// Stores the new value in web part properties
update(this.properties, propertyPath, (): any => { return newValue; });
@ -467,6 +466,9 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
// Call the default SPFx handler
this.onPropertyPaneFieldChanged(propertyPath);
// Refresh setting the right template for the property pane
await this._getTemplateContent();
// Refreshes the web part manually because custom fields don't update since sp-webpart-base@1.1.1
// https://github.com/SharePoint/sp-dev-docs/issues/594
if (!this.disableReactivePropertyChanges) {
@ -715,11 +717,55 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
await this._templateService.LoadHandlebarsHelpers(this.properties.useHandlebarsHelpers);
}
private async replaceQueryVariables(queryTemplate: string) {
const pagePropsVariables = /\{(?:Page)\.(.*?)\}/gi;
let reQueryTemplate = queryTemplate;
let match = pagePropsVariables.exec(reQueryTemplate);
let item = null;
if (match != null) {
let url = this.context.pageContext.web.absoluteUrl + `/_api/web/GetList(@v1)/RenderExtendedListFormData(itemId=${this.context.pageContext.listItem.id},formId='viewform',mode='2',options=7)?@v1='${this.context.pageContext.list.serverRelativeUrl}'`;
var client = this.context.spHttpClient;
try {
const response: SPHttpClientResponse = await client.post(url, SPHttpClient.configurations.v1, {});
if (response.ok) {
let result = await response.json();
let itemRow = JSON.parse(result.value);
item = itemRow.Data.Row[0];
}
else {
throw response.statusText;
}
} catch (error) {
Log.error(Text.format(LOG_SOURCE, "RenderExtendedListFormData"), error);
}
while (match !== null && item != null) {
// matched variable
let pageProp = match[1];
let itemProp;
if (pageProp.indexOf(".Label") !== -1 || pageProp.indexOf(".TermID") !== -1) {
let term = pageProp.split(".");
itemProp = item[term[0]][0][term[1]];
} else {
itemProp = item[pageProp];
}
if (itemProp.indexOf(' ') !== -1) {
// add quotes to multi term values
itemProp = `"${itemProp}"`;
}
queryTemplate = queryTemplate.replace(match[0], itemProp);
match = pagePropsVariables.exec(reQueryTemplate);
}
}
return queryTemplate;
}
public async render(): Promise<void> {
// Configure the provider before the query according to our needs
this._searchService.resultsCount = this.properties.maxResultsCount;
this._searchService.queryTemplate = this.properties.queryTemplate;
this._searchService.queryTemplate = await this.replaceQueryVariables(this.properties.queryTemplate);
this._searchService.resultSourceId = this.properties.resultSourceId;
this._searchService.sortList = this.properties.sortList;
this._searchService.enableQueryRules = this.properties.enableQueryRules;
@ -738,7 +784,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
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);
Log.warn(Text.format(LOG_SOURCE, this.instanceId), `The selected input value from the dynamic data source is not a string. Received (${typeof sourceValue})`, this.context.serviceScope);
}
}
}