[react-search-refiners] Miscellaneous updates (#695)
* * Migrated to SPFx 1.7.0 * Fixed sort feature * Added a sample TypeScript function to demonstrate NLP processing for the search query * Miscelleanous improvements * * Fixed wrong ids and dependencies * * Updated README * * Replaced JSOM taxonomy methods by the @pnp/sp-taxonomy counterparts + refactored refiners translation logic * Updated the filter panel to close on click out * Updgraded to @pnp 1.2.6 * Added a event listeners for hash change when the search box in bound to the 'URL fragment' SPFx builtin data source property so you can now build predefined filters with '#'. * Fix suggestions panel position to be absolute * * Quick fix on the search box * * Added a default query option (related to https://github.com/SharePoint/sp-dev-fx-webparts/issues/556) * * Added the ability to search by clicking on the search box icon
This commit is contained in:
parent
0265e4c7e6
commit
398d3b0363
|
@ -44,7 +44,5 @@ build.configureWebpack.mergeConfig({
|
|||
return generatedConfiguration;
|
||||
}
|
||||
});
|
||||
build.webpack.buildConfig
|
||||
build.addSuppression(new RegExp("\[sass\]",'g'));
|
||||
|
||||
build.initialize(gulp);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,11 +19,13 @@
|
|||
"@microsoft/sp-lodash-subset": "1.7.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.7.0",
|
||||
"@microsoft/sp-webpart-base": "1.7.0",
|
||||
"@pnp/common": "1.2.5",
|
||||
"@pnp/logging": "1.2.5",
|
||||
"@pnp/odata": "1.2.5",
|
||||
"@pnp/common": "1.2.6",
|
||||
"@pnp/logging": "1.2.6",
|
||||
"@pnp/odata": "1.2.6",
|
||||
"@pnp/polyfill-ie11": "1.0.0",
|
||||
"@pnp/sp": "1.2.5",
|
||||
"@pnp/sp": "1.2.6",
|
||||
"@pnp/sp-taxonomy": "1.2.6",
|
||||
"@pnp/sp-clientsvc": "1.2.6",
|
||||
"@pnp/spfx-controls-react": "1.10.0",
|
||||
"@pnp/spfx-property-controls": "1.12.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
|
@ -43,7 +45,7 @@
|
|||
"on-el-resize": "0.0.4",
|
||||
"react": "16.3.2",
|
||||
"react-ace": "6.1.4",
|
||||
"react-custom-scrollbars": "4.1.2",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-dom": "16.3.2",
|
||||
"react-js-pagination": "3.0.0",
|
||||
"video.js": "^7.3.0"
|
||||
|
|
|
@ -161,8 +161,9 @@ class SearchService implements ISearchService {
|
|||
const resultRows = r2.RawSearchResults.PrimaryQueryResult.RelevantResults.Table.Rows;
|
||||
let refinementResultsRows = r2.RawSearchResults.PrimaryQueryResult.RefinementResults;
|
||||
|
||||
const refinementRows = refinementResultsRows ? refinementResultsRows['Refiners'] : [];
|
||||
const refinementRows: any = refinementResultsRows ? refinementResultsRows.Refiners : [];
|
||||
if (refinementRows.length > 0) {
|
||||
|
||||
const component = await import(
|
||||
/* webpackChunkName: 'search-handlebars-helpers' */
|
||||
'handlebars-helpers'
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
interface ITaxonomyService {
|
||||
import { ITerm } from "@pnp/sp-taxonomy";
|
||||
|
||||
/**
|
||||
* Ensure all script dependencies are loaded before using the taxonomy SharePoint CSOM functions
|
||||
* https://dev.office.com/sharepoint/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom
|
||||
* @return {Promise<void>} A promise allowing you to execute your code logic.
|
||||
*/
|
||||
initialize();
|
||||
interface ITaxonomyService {
|
||||
|
||||
/**
|
||||
* Gets multiple terms by their ids using the current taxonomy context
|
||||
* @param termIds An array of term ids to search for
|
||||
* @return {Promise<SP.Taxonomy.TermCollection>} A promise containing the terms.
|
||||
* @return {Promise<ITerm[]>} A promise containing the terms.
|
||||
*/
|
||||
getTermsById(termIds: string[]): Promise<SP.Taxonomy.TermCollection>;
|
||||
getTermsById(termIds: string[]): Promise<ITerm[]>;
|
||||
}
|
||||
|
||||
export default ITaxonomyService;
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import ITaxonomyService from './ITaxonomyService';
|
||||
import { ITerm } from '@pnp/sp-taxonomy';
|
||||
|
||||
class MockTaxonomyService implements ITaxonomyService {
|
||||
|
||||
|
@ -11,7 +12,7 @@ class MockTaxonomyService implements ITaxonomyService {
|
|||
return p1;
|
||||
}
|
||||
|
||||
public getTermsById(termIds: string[]): Promise<SP.Taxonomy.TermCollection> {
|
||||
public getTermsById(termIds: string[]): Promise<ITerm[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,129 +1,38 @@
|
|||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { Logger, LogLevel } from '@pnp/logging';
|
||||
import { SPComponentLoader } from '@microsoft/sp-loader';
|
||||
import ITaxonomyService from './ITaxonomyService';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { ITermStore, ITerms, ITermData, Session, ITerm } from "@pnp/sp-taxonomy";
|
||||
|
||||
class TaxonomyService implements ITaxonomyService {
|
||||
|
||||
private _workingLanguageLcid: number;
|
||||
private _context: IWebPartContext;
|
||||
private _siteUrl: string;
|
||||
|
||||
public constructor(webPartContext: IWebPartContext, workingLanguage?: number){
|
||||
this._context = webPartContext;
|
||||
this._workingLanguageLcid = workingLanguage ? workingLanguage : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all script dependencies are loaded before using the taxonomy SharePoint CSOM functions
|
||||
* https://dev.office.com/sharepoint/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom
|
||||
* @return {Promise<void>} A promise allowing you to execute your code logic.
|
||||
*/
|
||||
public initialize(): Promise<void> {
|
||||
|
||||
const loadScriptPromise = new Promise<void>((resolve) => {
|
||||
|
||||
const siteCollectionUrl = this._context.pageContext.site.absoluteUrl;
|
||||
|
||||
SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/init.js', {
|
||||
globalExportsName: '$_global_init',
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "init.js", error));
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
|
||||
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/MicrosoftAjax.js', {
|
||||
globalExportsName: 'Sys'
|
||||
});
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "MicrosoftAjax.js", error));
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
|
||||
// The SP.Runtime.js file is needed in the hosted workbench environment
|
||||
// However, in a production environment, there will be an error message in the console saying the file is loaded twice
|
||||
// This is not a real issue for our purpose so we can keep these lines.
|
||||
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/SP.Runtime.js', {
|
||||
globalExportsName: 'SP'
|
||||
});
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "SP.Runtime.js", error));
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/SP.js', {
|
||||
globalExportsName: 'SP'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "SP.js", error));
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/SP.taxonomy.js', {
|
||||
globalExportsName: 'SP'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "SP.taxonomy.js", error));
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
// Hack the default method to pass the correct parameters to the server (bug in SP.taxonomy.js)
|
||||
// https://www.stephensaw.me/sharepoint-sp-taxonomy-js-term-getterms-not-working/
|
||||
const getTerms: any = function (g, h, e, d, c, f) {
|
||||
var a = this.get_context(), b;
|
||||
b = new SP.Taxonomy.TermCollection(a, new SP.ObjectPathMethod(a, this.get_path(), "GetTerms", [g, h, e, d, c, f]));
|
||||
return b;
|
||||
};
|
||||
|
||||
SP.Taxonomy.Term.prototype.getTerms = getTerms;
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return loadScriptPromise;
|
||||
public constructor(siteUrl: string){
|
||||
this._siteUrl = siteUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets multiple terms by their ids using the current taxonomy context
|
||||
* @param termIds An array of term ids to search for
|
||||
*/
|
||||
public getTermsById(termIds: string[]): Promise<SP.Taxonomy.TermCollection> {
|
||||
public async getTermsById(termIds: string[]): Promise<(ITerm & ITermData)[]> {
|
||||
|
||||
if (termIds.length > 0) {
|
||||
|
||||
const spContext = SP.ClientContext.get_current();
|
||||
const taxSession: SP.Taxonomy.TaxonomySession = SP.Taxonomy.TaxonomySession.getTaxonomySession(spContext);
|
||||
const termStore = taxSession.getDefaultSiteCollectionTermStore();
|
||||
|
||||
if (this._workingLanguageLcid) {
|
||||
termStore.set_workingLanguage(this._workingLanguageLcid);
|
||||
}
|
||||
|
||||
// The namespace SP is only available here (because of the init() method)
|
||||
const terms: SP.Taxonomy.TermCollection = termStore.getTermsById(termIds.map(t => new SP.Guid(t)));
|
||||
|
||||
// Additional properties can be loaded here
|
||||
spContext.load(terms, "Include(Id, Name)");
|
||||
|
||||
const p = new Promise<SP.Taxonomy.TermCollection>((resolve, reject) => {
|
||||
|
||||
spContext.executeQueryAsync(() => {
|
||||
resolve(terms);
|
||||
|
||||
}, (sender, args) => {
|
||||
const errorMessage = "[TaxonomyProvider.getTermById()]: Error: " + args.get_message();
|
||||
Logger.write(errorMessage, LogLevel.Error);
|
||||
reject(errorMessage);
|
||||
});
|
||||
const taxonomySession = new Session(this._siteUrl);
|
||||
taxonomySession.setup({
|
||||
sp: {
|
||||
headers: {
|
||||
Accept: "application/json;odata=nometadata",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return p;
|
||||
// Get the default termstore
|
||||
const store: ITermStore = await taxonomySession.getDefaultSiteCollectionTermStore();
|
||||
const terms: ITerms = await store.getTermsById(...termIds);
|
||||
|
||||
return await terms.select('Id','Labels').get();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
.searchBox {
|
||||
|
||||
position: relative;
|
||||
|
||||
.errorMessage {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
@ -12,10 +14,31 @@
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
.searchFieldGroup {
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.searchTextField {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.searchBtn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestionPanel {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.selected {
|
||||
|
|
|
@ -46,6 +46,8 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
rawInputValue: '',
|
||||
enhancedQuery: ''
|
||||
};
|
||||
|
||||
this._bindHashChange = this._bindHashChange.bind(this);
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
|
@ -53,6 +55,9 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
let inputValue = this.properties.defaultQueryKeywords.tryGetValue();
|
||||
|
||||
if (inputValue && typeof(inputValue) === 'string') {
|
||||
|
||||
// Notify subsscriber a new value if available
|
||||
this.context.dynamicDataSourceManager.notifyPropertyChanged('searchQuery');
|
||||
this._searchQuery.rawInputValue = inputValue;
|
||||
}
|
||||
|
||||
|
@ -119,6 +124,8 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
this.initSearchService();
|
||||
this.initNlpService();
|
||||
|
||||
this._bindHashChange();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -157,6 +164,16 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
protected onPropertyPaneFieldChanged(propertyPath: string) {
|
||||
this.initSearchService();
|
||||
this.initNlpService();
|
||||
|
||||
if (!this.properties.useDynamicDataSource) {
|
||||
this.properties.defaultQueryKeywords.setValue("");
|
||||
} else {
|
||||
this._bindHashChange();
|
||||
}
|
||||
|
||||
if (propertyPath === 'enableNlpService') {
|
||||
this.properties.enableDebugMode = !this.properties.enableDebugMode ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -352,4 +369,19 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
|
|||
|
||||
return searchQueryOptimizationFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to URL hash change if the dynamic property is set to the default 'URL Fragment' property
|
||||
*/
|
||||
private _bindHashChange() {
|
||||
|
||||
if (this.properties.defaultQueryKeywords.tryGetSource()) {
|
||||
if (this.properties.defaultQueryKeywords.reference.localeCompare('PageContext:UrlData:fragment') === 0) {
|
||||
// Manually subscribe to hash change since the default property doesn't
|
||||
window.addEventListener('hashchange', this.render);
|
||||
} else {
|
||||
window.removeEventListener('hashchange', this.render);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as update from 'immutability-helper';
|
|||
import styles from '../SearchBoxWebPart.module.scss';
|
||||
import ISearchQuery from '../../../models/ISearchQuery';
|
||||
import NlpDebugPanel from './NlpDebugPanel/NlpDebugPanel';
|
||||
import { IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||
|
||||
const SUGGESTION_CHAR_COUNT_TRIGGER = 3;
|
||||
|
||||
|
@ -48,45 +49,49 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
|||
selectedItem,
|
||||
highlightedIndex,
|
||||
openMenu,
|
||||
clearItems
|
||||
clearItems,
|
||||
}) => (
|
||||
<div>
|
||||
<TextField {...getInputProps({
|
||||
placeholder: strings.SearchInputPlaceholder,
|
||||
onKeyDown: event => {
|
||||
<div className={ styles.searchFieldGroup }>
|
||||
<TextField {...getInputProps({
|
||||
placeholder: strings.SearchInputPlaceholder,
|
||||
onKeyDown: event => {
|
||||
|
||||
// Submit search on "Enter"
|
||||
if (event.keyCode === 13 && (!isOpen || (isOpen && highlightedIndex === null))) {
|
||||
this._onSearch(this.state.searchInputValue);
|
||||
// Submit search on "Enter"
|
||||
if (event.keyCode === 13 && (!isOpen || (isOpen && highlightedIndex === null))) {
|
||||
this._onSearch(this.state.searchInputValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
})}
|
||||
value={ this.state.searchInputValue }
|
||||
autoComplete= "off"
|
||||
onChanged={ (value) => {
|
||||
})}
|
||||
className={ styles.searchTextField }
|
||||
value={ this.state.searchInputValue }
|
||||
autoComplete= "off"
|
||||
onChanged={ (value) => {
|
||||
|
||||
this.setState({
|
||||
searchInputValue: value,
|
||||
});
|
||||
this.setState({
|
||||
searchInputValue: value,
|
||||
});
|
||||
|
||||
if (this.state.selectedQuerySuggestions.length === 0) {
|
||||
clearItems();
|
||||
this._onChange(value);
|
||||
openMenu();
|
||||
} else {
|
||||
if (!value) {
|
||||
if (this.state.selectedQuerySuggestions.length === 0) {
|
||||
clearItems();
|
||||
this._onChange(value);
|
||||
openMenu();
|
||||
} else {
|
||||
if (!value) {
|
||||
|
||||
// Reset the selected suggestions if input is empty
|
||||
this.setState({
|
||||
selectedQuerySuggestions: [],
|
||||
});
|
||||
// Reset the selected suggestions if input is empty
|
||||
this.setState({
|
||||
selectedQuerySuggestions: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
iconProps={{
|
||||
iconName: 'Search',
|
||||
iconType: IconType.default
|
||||
}}/>
|
||||
}}/>
|
||||
<IconButton iconProps={{
|
||||
iconName: 'Search',
|
||||
iconType: IconType.default,
|
||||
}} onClick= {() => { this._onSearch(this.state.searchInputValue);} } className={ styles.searchBtn }>
|
||||
</IconButton>
|
||||
</div>
|
||||
{isOpen ?
|
||||
this.renderSuggestions(getItemProps, selectedItem, highlightedIndex)
|
||||
: null}
|
||||
|
@ -96,25 +101,30 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
|||
}
|
||||
|
||||
private renderBasicSearchBox(): JSX.Element {
|
||||
return <TextField
|
||||
placeholder={ strings.SearchInputPlaceholder }
|
||||
value={ this.state.searchInputValue }
|
||||
onChanged={ (value) => {
|
||||
this.setState({
|
||||
searchInputValue: value,
|
||||
});
|
||||
}}
|
||||
onKeyDown={ (event) => {
|
||||
return <div className={ styles.searchFieldGroup }>
|
||||
<TextField
|
||||
className={ styles.searchTextField }
|
||||
placeholder={ strings.SearchInputPlaceholder }
|
||||
value={ this.state.searchInputValue }
|
||||
onChanged={ (value) => {
|
||||
this.setState({
|
||||
searchInputValue: value,
|
||||
});
|
||||
}}
|
||||
onKeyDown={ (event) => {
|
||||
|
||||
// Submit search on "Enter"
|
||||
if (event.keyCode === 13) {
|
||||
this._onSearch(this.state.searchInputValue);
|
||||
}
|
||||
}}
|
||||
iconProps={{
|
||||
iconName: 'Search',
|
||||
iconType: IconType.default
|
||||
}}/>;
|
||||
// Submit search on "Enter"
|
||||
if (event.keyCode === 13) {
|
||||
this._onSearch(this.state.searchInputValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton iconProps={{
|
||||
iconName: 'Search',
|
||||
iconType: IconType.default,
|
||||
}} onClick= {() => { this._onSearch(this.state.searchInputValue);} } className={ styles.searchBtn }>
|
||||
</IconButton>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -251,47 +261,52 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
|
|||
*/
|
||||
public async _onSearch(queryText: string) {
|
||||
|
||||
let query: ISearchQuery = {
|
||||
rawInputValue: queryText,
|
||||
enhancedQuery: ''
|
||||
};
|
||||
// Don't send empty value
|
||||
if (queryText) {
|
||||
|
||||
this.setState({
|
||||
searchInputValue: queryText,
|
||||
});
|
||||
let query: ISearchQuery = {
|
||||
rawInputValue: queryText,
|
||||
enhancedQuery: ''
|
||||
};
|
||||
|
||||
if (this.props.enableNlpService && this.props.NlpService && queryText) {
|
||||
this.setState({
|
||||
searchInputValue: queryText,
|
||||
});
|
||||
|
||||
try {
|
||||
if (this.props.enableNlpService && this.props.NlpService && queryText) {
|
||||
|
||||
let enhancedQuery = await this.props.NlpService.enhanceSearchQuery(queryText, this.props.isStaging);
|
||||
query.enhancedQuery = enhancedQuery.enhancedQuery;
|
||||
try {
|
||||
|
||||
enhancedQuery.entities.map((entity) => {
|
||||
});
|
||||
let enhancedQuery = await this.props.NlpService.enhanceSearchQuery(queryText, this.props.isStaging);
|
||||
query.enhancedQuery = enhancedQuery.enhancedQuery;
|
||||
|
||||
this.setState({
|
||||
enhancedQuery: enhancedQuery,
|
||||
});
|
||||
enhancedQuery.entities.map((entity) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
// In case of failure, use the non-optimized query instead
|
||||
query.enhancedQuery = queryText;
|
||||
this.setState({
|
||||
enhancedQuery: enhancedQuery,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
// In case of failure, use the non-optimized query instead
|
||||
query.enhancedQuery = queryText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.searchInNewPage) {
|
||||
// Send the query to the a new via the query string
|
||||
const url = UrlHelper.addOrReplaceQueryStringParam(this.props.pageUrl, 'q', encodeURIComponent(queryText));
|
||||
if (this.props.searchInNewPage) {
|
||||
|
||||
// Send the query to the a new via the hash
|
||||
const url = `${this.props.pageUrl}#${encodeURIComponent(queryText)}`;
|
||||
|
||||
const behavior = this.props.openBehavior === PageOpenBehavior.NewTab ? '_blank' : '_self';
|
||||
window.open(url, behavior);
|
||||
|
||||
} else {
|
||||
const behavior = this.props.openBehavior === PageOpenBehavior.NewTab ? '_blank' : '_self';
|
||||
window.open(url, behavior);
|
||||
|
||||
} else {
|
||||
|
||||
// Notify the dynamic data controller
|
||||
this.props.onSearch(query);
|
||||
// Notify the dynamic data controller
|
||||
this.props.onSearch(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ define([], function() {
|
|||
return {
|
||||
"SearchInputPlaceholder": "Entrez vos termes de recherche...",
|
||||
"SearchBoxNewPage": "Options de la boîte de recherche",
|
||||
"SearchBoxEnableQuerySuggestions": "Activer les suggestions de recherche",
|
||||
"SearchBoxSearchInNewPageLabel": "Envoyer la requête sur une nouvelle page",
|
||||
"SearchBoxPageUrlLabel": "URL de la page",
|
||||
"SearchBoxUrlErrorMessage": "Veuillez spécifier une URL valide",
|
||||
|
|
|
@ -3,6 +3,8 @@ import { DynamicProperty } from '@microsoft/sp-component-base';
|
|||
|
||||
export interface ISearchResultsWebPartProps {
|
||||
queryKeywords: DynamicProperty<string>;
|
||||
defaultSearchQuery: string;
|
||||
useDefaultSearchQuery: boolean;
|
||||
queryTemplate: string;
|
||||
resultSourceId: string;
|
||||
sortList: string;
|
||||
|
|
|
@ -21,11 +21,11 @@
|
|||
},
|
||||
"title": {
|
||||
"default": "Search Results with Refiners",
|
||||
"fr-fr": "Résultats de recherche"
|
||||
"fr-fr": "Résultats de recherche"
|
||||
},
|
||||
"description": {
|
||||
"default": "Displays search results with customizable dynamic refiners",
|
||||
"fr-fr": "Affiche des résulats de recherche avec filtres personnalisables"
|
||||
"fr-fr": "Affiche des résulats de recherche avec filtres personnalisables"
|
||||
},
|
||||
"officeFabricIconFontName": "SearchAndApps",
|
||||
"properties": {
|
||||
|
@ -37,7 +37,8 @@
|
|||
"maxResultsCount": 10,
|
||||
"showBlank": true,
|
||||
"showResultsCount": true,
|
||||
"webPartTitle": ""
|
||||
"webPartTitle": "",
|
||||
"useDefaultSearchQuery": false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version, Text, Environment, EnvironmentType, DisplayMode, Log } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
|
@ -14,7 +14,9 @@ import {
|
|||
PropertyPaneToggle,
|
||||
PropertyPaneSlider,
|
||||
IPropertyPaneChoiceGroupOption,
|
||||
PropertyPaneChoiceGroup
|
||||
PropertyPaneChoiceGroup,
|
||||
PropertyPaneCheckbox,
|
||||
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import * as strings from 'SearchResultsWebPartStrings';
|
||||
import SearchResultsContainer from './components/SearchResultsContainer/SearchResultsContainer';
|
||||
|
@ -27,7 +29,6 @@ import TemplateService from '../../services/TemplateService/TemplateService';
|
|||
import { update, isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import MockSearchService from '../../services/SearchService/MockSearchService';
|
||||
import MockTemplateService from '../../services/TemplateService/MockTemplateService';
|
||||
import LocalizationHelper from '../../helpers/LocalizationHelper';
|
||||
import SearchService from '../../services/SearchService/SearchService';
|
||||
import TaxonomyService from '../../services/TaxonomyService/TaxonomyService';
|
||||
import MockTaxonomyService from '../../services/TaxonomyService/MockTaxonomyService';
|
||||
|
@ -35,7 +36,6 @@ import ISearchResultsContainerProps from './components/SearchResultsContainer/IS
|
|||
import { Placeholder, IPlaceholderProps } from '@pnp/spfx-controls-react/lib/Placeholder';
|
||||
import { SPHttpClientResponse, SPHttpClient } from '@microsoft/sp-http';
|
||||
|
||||
declare var System: any;
|
||||
const LOG_SOURCE: string = '[SearchResultsWebPart_{0}]';
|
||||
|
||||
export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchResultsWebPartProps> {
|
||||
|
@ -54,6 +54,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
constructor() {
|
||||
super();
|
||||
this._parseFieldListString = this._parseFieldListString.bind(this);
|
||||
|
||||
}
|
||||
|
||||
public async render(): Promise<void> {
|
||||
|
@ -83,6 +84,8 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
protected renderCompleted(): void {
|
||||
super.renderCompleted();
|
||||
|
||||
let queryKeywords;
|
||||
|
||||
let renderElement = null;
|
||||
if (typeof this.properties.useHandlebarsHelpers === 'undefined') {
|
||||
this.properties.useHandlebarsHelpers = true;
|
||||
|
@ -96,6 +99,12 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
this.context.propertyPane.refresh();
|
||||
}
|
||||
|
||||
if (!dataSourceValue) {
|
||||
queryKeywords = this.properties.defaultSearchQuery;
|
||||
} else {
|
||||
queryKeywords = dataSourceValue;
|
||||
}
|
||||
|
||||
const isValueConnected = !!this.properties.queryKeywords.tryGetSource();
|
||||
|
||||
const searchContainer: React.ReactElement<ISearchResultsContainerProps> = React.createElement(
|
||||
|
@ -103,7 +112,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
{
|
||||
searchService: this._searchService,
|
||||
taxonomyService: this._taxonomyService,
|
||||
queryKeywords: this.properties.queryKeywords.tryGetValue(),
|
||||
queryKeywords: queryKeywords,
|
||||
maxResultsCount: this.properties.maxResultsCount,
|
||||
resultSourceId: this.properties.resultSourceId,
|
||||
sortList: this.properties.sortList,
|
||||
|
@ -133,7 +142,9 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
}
|
||||
);
|
||||
|
||||
if (isValueConnected || (!isValueConnected && !isEmpty(this.properties.queryKeywords.tryGetValue()))) {
|
||||
if (isValueConnected && !this.properties.useDefaultSearchQuery ||
|
||||
isValueConnected && this.properties.useDefaultSearchQuery && this.properties.defaultSearchQuery ||
|
||||
!isValueConnected && !isEmpty(queryKeywords)) {
|
||||
renderElement = searchContainer;
|
||||
} else {
|
||||
if (this.displayMode === DisplayMode.Edit) {
|
||||
|
@ -155,10 +166,8 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
|
||||
} else {
|
||||
|
||||
const lcid = LocalizationHelper.getLocaleId(this.context.pageContext.cultureInfo.currentUICultureName);
|
||||
|
||||
this._searchService = new SearchService(this.context);
|
||||
this._taxonomyService = new TaxonomyService(this.context, lcid);
|
||||
this._taxonomyService = new TaxonomyService(this.context.pageContext.site.absoluteUrl);
|
||||
this._templateService = new TemplateService(this.context.spHttpClient, this.context.pageContext.cultureInfo.currentUICultureName);
|
||||
}
|
||||
|
||||
|
@ -167,6 +176,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
// Configure search query settings
|
||||
this._useResultSource = false;
|
||||
|
||||
|
||||
// Set the default search results layout
|
||||
this.properties.selectedLayout = this.properties.selectedLayout ? this.properties.selectedLayout : ResultsLayoutOption.List;
|
||||
|
||||
|
@ -234,6 +244,10 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
|
||||
protected async onPropertyPaneFieldChanged(propertyPath: string) {
|
||||
|
||||
if (!this.properties.useDefaultSearchQuery) {
|
||||
this.properties.defaultSearchQuery = '';
|
||||
}
|
||||
|
||||
if (propertyPath === 'selectedLayout') {
|
||||
// Refresh setting the right template for the property pane
|
||||
await this._getTemplateContent();
|
||||
|
@ -543,6 +557,30 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
*/
|
||||
private _getSearchQueryFields(): IPropertyPaneConditionalGroup {
|
||||
|
||||
let defaultSearchQueryFields: IPropertyPaneField<any>[] = [];
|
||||
|
||||
if (!!this.properties.queryKeywords.tryGetSource()) {
|
||||
defaultSearchQueryFields.push(
|
||||
PropertyPaneCheckbox('useDefaultSearchQuery', {
|
||||
text: strings.UseDefaultSearchQueryKeywordsFieldLabel
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.properties.useDefaultSearchQuery) {
|
||||
defaultSearchQueryFields.push(
|
||||
PropertyPaneTextField('defaultSearchQuery', {
|
||||
label: strings.DefaultSearchQueryKeywordsFieldLabel,
|
||||
description: strings.DefaultSearchQueryKeywordsFieldDescription,
|
||||
multiline: true,
|
||||
resizable: true,
|
||||
placeholder: strings.SearchQueryPlaceHolderText,
|
||||
onGetErrorMessage: this._validateEmptyField.bind(this),
|
||||
deferredValidationTime: 500
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
primaryGroup: {
|
||||
groupFields: [
|
||||
|
@ -561,20 +599,29 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
|
|||
groupFields: [
|
||||
PropertyPaneDynamicFieldSet({
|
||||
label: strings.SearchQueryKeywordsFieldLabel,
|
||||
|
||||
fields: [
|
||||
PropertyPaneDynamicField('queryKeywords', {
|
||||
label: strings.SearchQueryKeywordsFieldLabel
|
||||
label: strings.SearchQueryKeywordsFieldLabel
|
||||
})
|
||||
],
|
||||
sharedConfiguration: {
|
||||
depth: DynamicDataSharedDepth.Source,
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
}),
|
||||
].concat(defaultSearchQueryFields)
|
||||
},
|
||||
// Show the secondary group only if the web part has been
|
||||
// connected to a dynamic data source
|
||||
showSecondaryGroup: !!this.properties.queryKeywords.tryGetSource(),
|
||||
onShowPrimaryGroup: () => {
|
||||
|
||||
// Reset dynamic data fields related values to be consistent
|
||||
this.properties.useDefaultSearchQuery = false;
|
||||
this.properties.defaultSearchQuery = '';
|
||||
this.properties.queryKeywords.setValue('');
|
||||
this.render();
|
||||
}
|
||||
} as IPropertyPaneConditionalGroup;
|
||||
}
|
||||
|
||||
|
|
|
@ -133,12 +133,9 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
|
|||
isOpen={this.state.showPanel}
|
||||
type={PanelType.custom}
|
||||
customWidth="450px"
|
||||
isBlocking={false}
|
||||
isLightDismiss={true}
|
||||
onDismiss={this._onClosePanel}
|
||||
headerText={strings.FilterPanelTitle}
|
||||
closeButtonAriaLabel='Close'
|
||||
hasCloseButton={true}
|
||||
onRenderBody={() => {
|
||||
if (this.props.availableFilters.length > 0) {
|
||||
return (
|
||||
|
|
|
@ -15,6 +15,9 @@ import SearchResultsTemplate from '../Layouts/SearchResultsTemplate';
|
|||
import styles from '../SearchResultsWebPart.module.scss';
|
||||
import { SortPanel } from '../SortPanel';
|
||||
import { SortDirection } from "@pnp/sp";
|
||||
import { ITermData, ITerm } from '@pnp/sp-taxonomy';
|
||||
import LocalizationHelper from '../../../../helpers/LocalizationHelper';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
|
||||
declare var System: any;
|
||||
let FilterPanel = null;
|
||||
|
@ -282,7 +285,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
}
|
||||
} else {
|
||||
this.setState({
|
||||
areResultsLoading: false
|
||||
areResultsLoading: false,
|
||||
lastQuery: '',
|
||||
results: { RefinementResults: [], RelevantResults: [] },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -404,8 +409,12 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
|
|||
*/
|
||||
private async _getLocalizedFilters(rawFilters: IRefinementResult[]): Promise<IRefinementResult[]> {
|
||||
|
||||
// Get the current lcid according to current page language
|
||||
const lcid = LocalizationHelper.getLocaleId(this.props.context.pageContext.cultureInfo.currentUICultureName);
|
||||
|
||||
let termsToLocalize: { uniqueIdentifier: string, termId: string, localizedTermLabel: string }[] = [];
|
||||
let updatedFilters = [];
|
||||
let localizedTerms = [];
|
||||
|
||||
rawFilters.map((filterResult) => {
|
||||
|
||||
|
@ -432,45 +441,49 @@ 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.taxonomyService.initialize();
|
||||
// Get the terms from taxonomy
|
||||
// If a term doesn't exist anymore, it won't be retrieved by the API so the termValues count could be less than termsToLocalize count
|
||||
const termValues = await this.props.taxonomyService.getTermsById(termsToLocalize.map((t) => { return t.termId; }));
|
||||
|
||||
const termsEnumerator = termValues.getEnumerator();
|
||||
termsToLocalize.map((termToLocalize) => {
|
||||
|
||||
while (termsEnumerator.moveNext()) {
|
||||
// Check if the term has been retrieved from taxonomy (i.e. exists)
|
||||
const termsFromTaxonomy = termValues.filter((taxonomyTerm: ITerm & ITermData) => {
|
||||
const termIdFromTaxonomy = taxonomyTerm.Id.substring(taxonomyTerm.Id.indexOf('(') + 1, taxonomyTerm.Id.indexOf(')'));
|
||||
return termIdFromTaxonomy === termToLocalize.termId;
|
||||
});
|
||||
|
||||
const currentTerm = termsEnumerator.get_current();
|
||||
if (termsFromTaxonomy.length > 0) {
|
||||
|
||||
// Need to do this check in the case where the term indexed by the search doesn't exist anymore in the term store
|
||||
if (!currentTerm.get_serverObjectIsNull()) {
|
||||
// Should be always unique since we can't have two terms with the same ids
|
||||
const termFromTaxonomy: ITerm & ITermData = termsFromTaxonomy[0];
|
||||
|
||||
const termId = currentTerm.get_id();
|
||||
|
||||
// Check if retrieved term is part of terms to localize
|
||||
const terms = termsToLocalize.filter((e) => { return e.termId === termId.toString(); });
|
||||
if (terms.length > 0) {
|
||||
termsToLocalize = termsToLocalize.map((term) => {
|
||||
if (term.termId === terms[0].termId) {
|
||||
return {
|
||||
uniqueIdentifier: term.uniqueIdentifier,
|
||||
termId: termId.toString(),
|
||||
localizedTermLabel: termsEnumerator.get_current().get_name(),
|
||||
};
|
||||
} else {
|
||||
return term;
|
||||
}
|
||||
});
|
||||
}
|
||||
// It supposes the 'Label' property has been selected in the underlying call
|
||||
// A term always have a default label so the collection can't be empty
|
||||
const localizedLabel = termFromTaxonomy["Labels"]._Child_Items_.filter((label: any) => {
|
||||
return label.Language === lcid;
|
||||
});
|
||||
|
||||
localizedTerms.push({
|
||||
uniqueIdentifier: termToLocalize.uniqueIdentifier,
|
||||
termId: termToLocalize.termId,
|
||||
localizedTermLabel: localizedLabel.length > 0 ? localizedLabel[0].Value : termFromTaxonomy.Name
|
||||
});
|
||||
} else {
|
||||
localizedTerms.push({
|
||||
uniqueIdentifier: termToLocalize.uniqueIdentifier,
|
||||
termId: termToLocalize.termId,
|
||||
localizedTermLabel: Text.format(strings.TermNotFound, termToLocalize.termId)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update original filters with localized values
|
||||
rawFilters.map((filter) => {
|
||||
let updatedValues = [];
|
||||
|
||||
filter.Values.map((value) => {
|
||||
const existingFilters = termsToLocalize.filter((e) => { return e.uniqueIdentifier === value.RefinementToken; });
|
||||
const existingFilters = localizedTerms.filter((e) => { return e.uniqueIdentifier === value.RefinementToken; });
|
||||
if (existingFilters.length > 0) {
|
||||
updatedValues.push({
|
||||
RefinementCount: value.RefinementCount,
|
||||
|
|
|
@ -63,6 +63,10 @@ define([], function() {
|
|||
"SortPanelSortFieldAria":"Select a field",
|
||||
"SortPanelSortFieldPlaceHolder":"Select a field",
|
||||
"SortPanelSortDirectionLabel":"Sort Direction",
|
||||
}
|
||||
},
|
||||
"TermNotFound": "(Term with ID '{0}' not found)",
|
||||
"UseDefaultSearchQueryKeywordsFieldLabel": "Use a default search query",
|
||||
"DefaultSearchQueryKeywordsFieldLabel": "Default search query",
|
||||
"DefaultSearchQueryKeywordsFieldDescription": "This query will be used when the data source value is still empty."
|
||||
}
|
||||
});
|
|
@ -63,6 +63,10 @@ define([], function() {
|
|||
"SortPanelSortFieldAria":"Sélectionner un champ",
|
||||
"SortPanelSortFieldPlaceHolder":"Sélectionner un champ",
|
||||
"SortPanelSortDirectionLabel":"Direction de tri",
|
||||
}
|
||||
},
|
||||
"TermNotFound": "(Terme avec l'ID '{0}' non trouvé)",
|
||||
"UseDefaultSearchQueryKeywordsFieldLabel": "Utiliser une requête initiale",
|
||||
"DefaultSearchQueryKeywordsFieldLabel": "Requête de recherche par défaut",
|
||||
"DefaultSearchQueryKeywordsFieldDescription": "Cette requête sera utilisée par défault dans le cas où la valeur de la source de données est encore vide."
|
||||
}
|
||||
});
|
|
@ -62,7 +62,11 @@ declare interface ISearchResultsWebPartStrings {
|
|||
SortPanelSortFieldAria:string;
|
||||
SortPanelSortFieldPlaceHolder:string;
|
||||
SortPanelSortDirectionLabel:string;
|
||||
}
|
||||
},
|
||||
TermNotFound: string;
|
||||
UseDefaultSearchQueryKeywordsFieldLabel: string;
|
||||
DefaultSearchQueryKeywordsFieldLabel: string;
|
||||
DefaultSearchQueryKeywordsFieldDescription: string;
|
||||
}
|
||||
|
||||
declare module 'SearchResultsWebPartStrings' {
|
||||
|
|
Loading…
Reference in New Issue