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:
parent
910d377a77
commit
27977d9235
|
@ -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.<column>} - 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.
|
||||
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue