[react-search-refiners] Migrated to SPFx 1.7.0 + Added LUIS Azure Function (#677)
* * 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
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.6.0-plusbeta",
|
||||
"libraryName": "react-search-refiners",
|
||||
"libraryId": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"environment": "spo",
|
||||
"isCreatingSolution": true,
|
||||
"componentType": "webpart",
|
||||
"extensionType": "ApplicationCustomizer",
|
||||
"packageManager": "npm"
|
||||
}
|
||||
}
|
|
@ -1,16 +1,39 @@
|
|||
# SharePoint Framework search with search box, refiners and paging sample
|
||||
|
||||
## Summary
|
||||
This sample shows you how to build user friendly SharePoint search experiences using Office UI fabric tiles, custom refiners, paging and suggestions.
|
||||
This sample shows you how to build user friendly SharePoint search experiences using SPFx in the modern interface. The main features include:
|
||||
|
||||
- Fully customizable SharePoint search query like the good old Content Search Web Part.
|
||||
- Can either use a static query or be connected to a search box component using SPFx dynamic data.
|
||||
- Live templating system with Handlebar to meet your requirements in terms of UI + builtin list and tiles templates. Can alos use template from an external file.
|
||||
- Search results includings previews for Office documents and Office 365 videos.
|
||||
- Customizable refiners supporting multilingual values for taxonomy based filters.
|
||||
- Sortable results (unique field).
|
||||
- Results paging.
|
||||
- SharePoint best bets support.
|
||||
- Search query enhancement with NLP tools (like Microsoft LUIS).
|
||||
|
||||
<p align="center">
|
||||
<img src="./images/react-search-refiners.gif"/>
|
||||
</p>
|
||||
|
||||
An associated [blog post](http://thecollaborationcorner.com/2017/10/16/build-dynamic-sharepoint-search-experiences-with-refiners-and-paging-with-spfx-office-ui-fabric-and-pnp-js-library/) is available to give you more details about this sample implementation.
|
||||
This sample includes the following components and service(s):
|
||||
|
||||
**Web Part(s)**
|
||||
|
||||
Component | Description
|
||||
----- | -----
|
||||
Search Box Web Part | Allows users to enter free text/KQL search queries connected to a search results Web Part.
|
||||
Search Results Web Part | Performs static or dynamic search query with customizable parameters like refiners, sorting and templating. An associated [blog post](http://thecollaborationcorner.com/2017/10/16/build-dynamic-sharepoint-search-experiences-with-refiners-and-paging-with-spfx-office-ui-fabric-and-pnp-js-library/) is available to give you more details about this Web Part implementation.
|
||||
|
||||
**Back-end service(s)**
|
||||
|
||||
Service | Description
|
||||
----- | -----
|
||||
Search Query Enhancer | Sample Azure function to demonstrate the use of Microsoft LUIS and other cognitive services to interpret user intents and enhance the search box query accordingly.
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-1.6.0--plusbeta-blue.svg)
|
||||
![drop](https://img.shields.io/badge/drop-1.7.0-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
|
@ -33,10 +56,11 @@ Version|Date|Comments
|
|||
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.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 toggle to load/use handlebars helpers/moment.</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>
|
||||
2.1.2.0 | Nov 9, 2018 | <ul><li>Bug fix for IE11.</li><li>Added date query variables.</li><li>Added support for both result id and query template.</li><li>Added `getUniqueCount` helper function.</li></ul>
|
||||
2.1.2.0 | Nov 9, 2018 | <ul><li>Bug fix for IE11.</li><li>Added date query variables.</li><li>Added support for both result source id and query template.</li><li>Added `getUniqueCount` helper function.</li></ul>
|
||||
2.2.0.0 | Nov 11, 2018 | <ul><li>Upgraded to SPFx 1.7.0</li><li>Added a TypeScript Azure Function to demonstrate NLP processing on search query</li><li>Removed extension data source. Now we use the default SPFx 'Page Environment' data source.</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.**
|
||||
|
@ -46,13 +70,47 @@ Version|Date|Comments
|
|||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
|
||||
### SPFx
|
||||
- Go to the [spfx](./spfx) directory
|
||||
- In the command line run:
|
||||
- `npm install`
|
||||
- `gulp serve`
|
||||
|
||||
### Web Part Configuration ###
|
||||
### Azure Function (Not mandatory)
|
||||
|
||||
The following settings are available in the Web Part property pane:
|
||||
- Go to the [functions](./functions) directory
|
||||
- Follow the README.md file instructions
|
||||
- Set the correct service URL in the Search Box Web Part
|
||||
|
||||
## Web Parts Configuration
|
||||
|
||||
### Search Box Web Part
|
||||
|
||||
<p align="center"><img width="300px" src="./images/sb_property_pane.png"/><p>
|
||||
|
||||
#### Default Search Query Settings
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Use a dynamic data source | You can set a default query text coming from am other data source. This case is particularly useful when you want to put a search box Web Part on the front page redirecting to an other page with the same query. Use the query string parameter 'q' from the builtin 'Page Environment' data source.
|
||||
|
||||
#### Search box options
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Enable query suggestions | The search box supports query suggestions from SharePoint. Refer to the following [article](https://docs.microsoft.com/en-us/sharepoint/search/manage-query-suggestions) to know how to add query suggestions in your SharePoint tenant (caution: it can take up to 24h for changes to take effect).
|
||||
Send the query to a new page | Sends the search query text to a new page. On that page, use an other searh box Web Part configured with a dynamic data source as the default query. This Web Part uses the 'q' query string parameter.
|
||||
|
||||
#### Search query enhancement
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Use Natural Language Processing service | Turn this option 'on' if you want to enhance the query text with NLP services like LUIS. In the _'Service Url'_ field, enter the URL of the Azure Function endpoint. Refer the instructions in the `'/functions/README.md'` file to set up the service. In this sample, only relevant detected keywords are returned as q new query using LUIS. Enabling debug mode will show you relevant information about the entered query.
|
||||
|
||||
---
|
||||
|
||||
### Search Results Web Part
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
|
@ -68,36 +126,28 @@ The following settings are available in the Web Part property pane:
|
|||
</tr>
|
||||
<table>
|
||||
|
||||
#### Search Query Configuration ####
|
||||
#### Search Query Configuration
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Search query keywords | Here you choose to use a static search query or a query coming from a search box Web Part on a page or the "q" URL query string parameter. The search query is in KQL format so you can use search query variables (See this [post](http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html) to know which ones are allowed). You can only plug one source to this Web Part.
|
||||
Search query keywords | Here you choose to use a static search query or a query coming from a data source. It is recommended to use the associated Web Part coming with this sample. The search query is in KQL format so you can use search query variables (See this [post](http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html) to know which ones are allowed). You can only plug one source to this Web Part.
|
||||
|
||||
<p align="center"><img src="./images/wp_connection.png"/><p>
|
||||
<p align="center"><img width="300px" src="./images/wp_connection.png"/><p>
|
||||
|
||||
#### Search Settings ####
|
||||
#### Search Settings
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Query template | The search query template in KQL format. You can use search variables here (like Path:{Site}).
|
||||
Result Source Identifier | The GUID of a SharePoint result source. If you specify a value here, query template and query keywords won't be applied. Otherwise the default SharePoint result source is used.
|
||||
Enable Query Rules | Enable the query rules if applies
|
||||
Selected properties | The search managed properties to retrieve. You can use these properties then in the code like this (`item.property_name`).
|
||||
Refiners | The search managed properties to use as refiners. Make sure these are refinable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the refnement panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",...
|
||||
Result Source Identifier | The GUID of a SharePoint result source.
|
||||
Initial sort order | The initial search results sort order. You can use mutliple properties here.
|
||||
Sortable fields | The search managed properties to use for sorting. Make sure these are sortable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the sort panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",... If no sortable fields are provided, the 'Sort' button will not be visible.
|
||||
Enable Query Rules | Enable the query rules if applies. Turn this options 'on' to display your SharePoint Promoted results (links only).
|
||||
Selected properties | The search managed properties to retrieve. You can use these properties then in your Handlebar template with the syntax (`item.property_name`).
|
||||
Refiners | The search managed properties to use as refiners. Make sure these are refinable. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.). The order is the same as they will appear in the refnement panel. You can also provide your own custom labels using the following format RefinableString01:"You custom filter label",RefinableString02:"You custom filter label",... This Web Part supports dynamic translation of taxonomy based refiners with few additional configurations (see below).
|
||||
Number of items to retrieve per page | Quite explicit. The paging behavior is done directly by the search API (See the *SearchDataProvider.ts* file), not by the code on post-render.
|
||||
|
||||
#### Styling Options ####
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Show blank if no result | Shows nothing if there is no result
|
||||
Show result count | Shows the result count and entered keywords
|
||||
Show paging | Indicates whether or not the component should show the paging control at the bottom.
|
||||
Result Layouts options | Choose the template to use to display search results. Some layouts are defined by default (List oand Tiles) but you can create your own either by clinkg on the **"Custom"** tile, or **"Edit template"** from an existing chosen template. In custom mode, you can set an external template. It has to be in the same SharePoint tenant. Behind the scenes, the Office UI Fabric core CSS components are used in a isolated way.
|
||||
|
||||
### Taxonomy values dynamic translation
|
||||
##### Miscellaneous: Taxonomy values dynamic translation
|
||||
|
||||
This Web Part supports the translation for taxonomy based filters according to current site language. To get it work, you must map a new refinable managed property associated with *ows_taxId_<your_column_name>* crawled property.
|
||||
|
||||
|
@ -105,19 +155,52 @@ This Web Part supports the translation for taxonomy based filters according to c
|
|||
<img src="./images/managed-property.png"/>
|
||||
</p>
|
||||
|
||||
### Query suggestions
|
||||
#### Styling Options
|
||||
|
||||
The search box supports query suggestions from SharePoint. Refer to the following [article](https://docs.microsoft.com/en-us/sharepoint/search/manage-query-suggestions) to know how to add query suggestions in SharePoint (caution: it can take up to 24h for changes to take effect).
|
||||
Setting | Description
|
||||
-------|----
|
||||
Web Part Title | Shows a title for this Web Part. Set blank if you don't want a title.
|
||||
Show blank if no result | Shows nothing if there is no result
|
||||
Show result count | Shows the result count and entered keywords
|
||||
Show paging | Indicates whether or not the component should show the paging control at the bottom.
|
||||
Result Layouts options | Choose the template to use to display search results. Some layouts are defined by default (List oand Tiles) but you can create your own either by clinkg on the **"Custom"** tile, or **"Edit template"** from an existing chosen template. In custom mode, you can set an external template. It has to be in the same SharePoint tenant. Behind the scenes, the Office UI Fabric core CSS components are used in a isolated way.
|
||||
Handlebars Helpers | Load [handlebar helpers](https://github.com/helpers/handlebars-helpers) to use in your template. Disable this option will make Web Part loading faster if you don't need them.
|
||||
|
||||
### Templates with Handlebars ###
|
||||
---
|
||||
|
||||
#### Templates with Handlebars
|
||||
|
||||
This Web Part allows you change customize the way you display your search results. The templating feature comes directly from the original [react-content-query-webpart](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-content-query-webpart) so thanks to @spplante!
|
||||
|
||||
<p align="center">
|
||||
<img src="./images/edit_template.png"/>
|
||||
<img width="500px" src="./images/edit_template.png"/>
|
||||
</p>
|
||||
|
||||
##### Available tokens
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
`{{showResultsCount}}` | Boolean flag corresponding to the associated in the property pane.
|
||||
`{{totalRows}}` | The result count.
|
||||
`{{maxResultsCount}}` | The number of results configured to retrieve in the web part.
|
||||
`{{actualResultsCount}}` | The actual number of results retreived.
|
||||
`{{keywords}}` | The search query.
|
||||
`{{getSummary HitHighlightedSummary}}` | Format the *HitHighlightedSummary* property with recognized words in bold.
|
||||
`{{getDate <date_managed_property> "<format>}}"` | Format the date with moment.ts according to the current language.
|
||||
`{{getPreviewSrc item}}` | Determine the image thumbnail URL if applicable.
|
||||
`{{getUrl item}}` | Get the item URL. For a document, it means the URL to the Office Online instance or the direct URL (to download it).
|
||||
`{{getUrlField managed_propertyOWSURLH "URL/Title"}}` | Return the URL or Title part of a URL field managed property.
|
||||
`{{getCountMessage totalRows <?keywords>}}` | Display a friendly message displaying the result and the entered keywords.
|
||||
`{{<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.
|
||||
|
||||
#### Query variables
|
||||
|
||||
The following out of the box query variables are supported/tested:
|
||||
|
||||
* {searchTerms}
|
||||
|
@ -144,11 +227,11 @@ The following custom query variables are supported:
|
|||
This WP supports SharePoint best bets via SharePoint query rules:
|
||||
|
||||
<p align="center">
|
||||
<img src="./images/query_rules.png"/>
|
||||
<img width="500px" src="./images/query_rules.png"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./images/best_bets.png"/>
|
||||
<img width="500px" src="./images/best_bets.png"/>
|
||||
</p>
|
||||
|
||||
#### Elements previews
|
||||
|
@ -156,7 +239,7 @@ This WP supports SharePoint best bets via SharePoint query rules:
|
|||
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"/>
|
||||
<img width="500px" 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):
|
||||
|
@ -168,29 +251,6 @@ The WebPart must have the following selected properties in the configuration to
|
|||
|
||||
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
|
||||
-------|----
|
||||
`{{showResultsCount}}` | Boolean flag corresponding to the associated in the property pane.
|
||||
`{{totalRows}}` | The result count.
|
||||
`{{maxResultsCount}}` | The number of results configured to retrieve in the web part.
|
||||
`{{actualResultsCount}}` | The actual number of results retreived.
|
||||
`{{keywords}}` | The search query.
|
||||
`{{getSummary HitHighlightedSummary}}` | Format the *HitHighlightedSummary* property with recognized words in bold.
|
||||
`{{getDate <date_managed_property> "<format>}}"` | Format the date with moment.ts according to the current language.
|
||||
`{{getPreviewSrc item}}` | Determine the image thumbnail URL if applicable.
|
||||
`{{getUrl item}}` | Get the item URL. For a document, it means the URL to the Office Online instance or the direct URL (to download it).
|
||||
`{{getUrlField managed_propertyOWSURLH "URL/Title"}}` | Return the URL or Title part of a URL field managed property.
|
||||
`{{getCountMessage totalRows <?keywords>}}` | Display a friendly message displaying the result and the entered keywords.
|
||||
`{{<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.
|
||||
|
||||
## Features
|
||||
This Web Part illustrates the following concepts on top of the SharePoint Framework:
|
||||
|
||||
|
@ -203,4 +263,4 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
|
|||
- Use the React container component approach inspiring by the [react-todo-basic sample](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-todo-basic).
|
||||
- Use [on-el-resize](https://www.npmjs.com/package/on-el-resize) by [Andrew Koltyakov](https://github.com/koltyakov) to resize iframes dynamically
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-search-refiners" />
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-search-refiners" />
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"search-results": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/searchResults/SearchResultsWebPart.js",
|
||||
"manifest": "./src/webparts/searchResults/SearchResultsWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"search-box": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/searchBox/SearchBoxWebPart.js",
|
||||
"manifest": "./src/webparts/searchBox/SearchBoxWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"search-query-string": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/extensions/queryStringDataSource/QueryStringDataSourceApplicationCustomizer.js",
|
||||
"manifest": "./src/extensions/queryStringDataSource/QueryStringDataSourceApplicationCustomizer.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"localizedResources": {
|
||||
"SearchWebPartStrings": "lib/webparts/searchResults/loc/{locale}.js",
|
||||
"PropertyControlStrings": "./node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||
"SearchBoxWebPartStrings": "lib/webparts/searchBox/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
|
||||
"QueryStringDataSourceApplicationCustomizerStrings": "lib/extensions/queryStringDataSource/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-search-refiners",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "PnP - Search Web Parts",
|
||||
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"version": "2.1.2.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": false,
|
||||
"features": [
|
||||
{
|
||||
"title": "Application Extension - Deployment of custom action.",
|
||||
"description": "Deploys a custom action with ClientSideComponentId association",
|
||||
"id": "b28f9c2b-6e10-4764-8585-5f8e6533001b",
|
||||
"version": "1.0.0.0",
|
||||
"assets": {
|
||||
"elementManifests": [
|
||||
"elements.xml"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/pnp-react-search-refiners.sppkg"
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
},
|
||||
"serveConfigurations": {
|
||||
"default": {
|
||||
"pageUrl": "https://localhost:5432/workbench"
|
||||
},
|
||||
"queryStringDataSource": {
|
||||
"pageUrl": "https://collaborationcorner.sharepoint.com/teams/PnPIntranet/_layouts/15/workbench.aspx",
|
||||
"customActions": {
|
||||
"24cae67d-dec7-4eff-bb41-49451d5b5a11": {
|
||||
"location": "ClientSideExtension.ApplicationCustomizer",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
dist/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
|
||||
# build tools
|
||||
coverage/
|
||||
test-report.xml
|
||||
|
||||
# misc
|
||||
npm-debug.log
|
||||
yarn-error.log
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"bitwise": true,
|
||||
"eqeqeq": true,
|
||||
"forin": true,
|
||||
"noarg": true,
|
||||
"noempty": true,
|
||||
"nonbsp": true,
|
||||
"nonew": true,
|
||||
"undef": true,
|
||||
"varstmt": true,
|
||||
"esversion": 6,
|
||||
"latedef": true,
|
||||
"unused": true,
|
||||
"indent": 2,
|
||||
"quotmark": "single",
|
||||
"maxcomplexity": 20,
|
||||
"maxlen": 140,
|
||||
"maxerr": 50,
|
||||
"globals": {},
|
||||
"strict": true,
|
||||
"laxbreak": true,
|
||||
"browser": true,
|
||||
"module": true,
|
||||
"node": true,
|
||||
"trailing": true,
|
||||
"onevar": true,
|
||||
"white": true
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Local Azure Function",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 5858,
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/dist/**/*.js" ]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Jest all tests",
|
||||
"program": "${workspaceRoot}/node_modules/jest/bin/jest",
|
||||
"args": ["--runInBand"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
# PnP - Search Query Enhancer
|
||||
|
||||
## Description
|
||||
|
||||
This sample demonstrates the following principles:
|
||||
- Create an Azure function using TypeScript and Webpack. The original setup was reused from this [article](https://medium.com/burak-tasci/backend-development-on-azure-functions-with-typescript-56113b6be4b9) with only few adjustments.
|
||||
- Connect Azure Function to an SPFx component
|
||||
- Use third party back end services like Microsoft LUIS or Text Analysis to interpret a search query and enhance it with NLP services.
|
||||
|
||||
***In this sample, the function is secured by a function code. For production use, refer to [this article](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient-enterpriseapi) to protect and use it with Azure AD and SPFx.***
|
||||
|
||||
***In real world scenarios, you may want add your own intents and build your enhanced search queries accordingly. Use this sample as a starter.***
|
||||
|
||||
## Why LUIS instead of SharePoint search query rules?
|
||||
|
||||
- Easy to manage for power users .They don't have to deal with complex SharePoint concepts. With LUIS, they can manage and refine the model more easily in a friendly comprehensive interface.
|
||||
- Real time monitoring. Power users can review utterances submitted by end users in the LUIS portal and what keywords are entered. They can add new terms as synonyms automatically from the utterances and identify new intentions more precisely.
|
||||
- Extensible model with custom intents mapped to predefined well know SharePoint search queries.
|
||||
- Able to plug in the Bing Spell checker automatically to correct mispeleld words and get a clean query
|
||||
|
||||
## Set up the solution
|
||||
|
||||
- In the [www.luis.ai](www.luis.ai) portal, imports new applications from the JSON files in the [/luis](./luis) folder.
|
||||
|
||||
<p align="center"><img width="500px" src="../images/luis_apps.png"/><p>
|
||||
|
||||
- In Azure, create keys for the following Microsoft Cognitive Services:
|
||||
- Language Understanding
|
||||
- Bing Spell Check v7
|
||||
- Text Analytics
|
||||
|
||||
<p align="center"><img width="500px" src="../images/azure_keys.png"/><p>
|
||||
|
||||
- Fill the following values in the `local.settings.json` file according to your environment:
|
||||
|
||||
| Setting | Description
|
||||
| ------- | ---------
|
||||
LUIS_SubscriptionKey | The key value for LUIS retrieved from the Azure portal
|
||||
LUIS_AzureRegion | Azure region where you created the LUIS key
|
||||
Bing_SpellCheckApiKey | The Bing Spell Check API key retrieved from the Azure portal
|
||||
TextAnalytics_SubscriptionKey | The key value for Text Analytics Service retrieved from the Azure portal
|
||||
TextAnalytics_AzureRegion | Azure region where you created the Text Analytics key
|
||||
|
||||
- Add keys to your LUIS applications
|
||||
|
||||
<p align="center"><img width="500px" src="../images/luis_key_manage.png"/><p>
|
||||
|
||||
- Train and publish the LUIS applications
|
||||
- Fill LUIS app ids in the `luismappings.json` according to your environment
|
||||
- Play with the function!
|
||||
|
||||
### Intents
|
||||
|
||||
| Intent | Description
|
||||
| ------ | -----------
|
||||
| PnP.SearchByKeywords | The default intent for the search query. Used to improve free text searches for SharePoint (90% of users queries in the portal).
|
||||
| None | Needed to avoid unrelevant query such as noise words, trolling or insulting words
|
||||
|
||||
### Entities
|
||||
|
||||
| Entity | Type | Description | Recognition method |
|
||||
| ------ | ---- | ----------- | ------------ |
|
||||
| [keyPhrase](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-quickstart-intent-and-key-phrase) | Builtin | This prebuilt enity catches important keywords in the phrase. In this case, we treat these values as a "free" keyword which will be matched with all relevant SharePoint search managed properties. | Machine Learning
|
||||
|
||||
## How to debug this function locally ? ##
|
||||
|
||||
### Prerequisites ###
|
||||
|
||||
- In VSCode, open the root folder `./functions`.
|
||||
- Install all dependencies using `npm i`.
|
||||
- Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest) on youre machine.
|
||||
- Install Azure Function Core tools globaly using `npm install -g azure-functions-core-tools@2` (version 2).
|
||||
- In a Node.js console, build the solution using `npm run build:dev` cmd. For production use, execute `npm run build` (minified version of the JS code).
|
||||
- In a Node.js console, from the `pnp-query-enhancer/dist` folder, run the following command `func start`.
|
||||
- In VSCode, launch the *'Debug Local Azure Function'* debug configuration
|
||||
- Set breakpoints directly in your **'.ts'** files
|
||||
- Send your requests either using Postman with the localhost address according to your settings (i.e. `http://localhost:7071/api/enhanceQuery`) or directly in the 'Search Box Webpart' via the 'Service URL' parameter.
|
||||
- Enjoy ;)
|
||||
|
||||
### Azure Function Proxy configuration ###
|
||||
|
||||
This solution uses an Azure function proxy to get an only single endpoint URL for multiple functions. See the **proxies.json** file to see defined routes.
|
||||
|
||||
## How to deploy the solution to Azure ? ##
|
||||
|
||||
### Development scenario
|
||||
|
||||
We recommend to use Visual Studio Code to work with this solution.
|
||||
|
||||
- In VSCode, download the [Azure Function](https://code.visualstudio.com/tutorials/functions-extension/getting-started) extension
|
||||
- Sign-in to to Azure account into the extension
|
||||
- In a Node.js console, build the application using the command `npm run build` (minified version)
|
||||
- Use the **"Deploy to Function App"** feature (in the extension top bar) using the *'dist'* folder. Make sure you've run the `npm run build` cmd before.
|
||||
- Upload the application settings (`local.settings.json`)
|
||||
|
||||
### Production scenario with CI
|
||||
|
||||
A `deploy.ps1` script is available to also deploy this function into your Azure environment.
|
||||
|
||||
- From you Azure portal, create a new empty function
|
||||
- Set the `Azure_Function_Name` value in the `local.settings.json` accordingly.
|
||||
- Login to Azure using `az login` then run `deploy.ps1` script with your parameters.
|
||||
|
||||
***In both scenarios, you can test your function using Postman. If you test it using a SPFx component, don't forget to add the SharePoint domain to the CORS settings to allow this origin:***
|
||||
|
||||
<p align="center"><img width="500px" src="../images/cors_settings.png"/><p>
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
[CmdletBinding()]
|
||||
Param (
|
||||
|
||||
[Parameter(Mandatory = $False)]
|
||||
[switch]$OverwriteSettings,
|
||||
|
||||
[Parameter(Mandatory = $False)]
|
||||
[switch]$Minified
|
||||
)
|
||||
|
||||
$0 = $myInvocation.MyCommand.Definition
|
||||
$CommandDirectory = [System.IO.Path]::GetDirectoryName($0)
|
||||
|
||||
$AppSettingsFilePath = Join-Path -Path $CommandDirectory -ChildPath "src\local.settings.json"
|
||||
$AppSettings = Get-Content -Path $AppSettingsFilePath -Raw | ConvertFrom-Json
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# Execute tests
|
||||
npm run test:ci 2>&1 | Write-Host
|
||||
|
||||
if ($LASTEXITCODE -eq 1) {
|
||||
throw "Error during tests!"
|
||||
}
|
||||
|
||||
# Build the solution
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($Minified.IsPresent) {
|
||||
npm run build
|
||||
} else {
|
||||
npm run build:dev
|
||||
}
|
||||
|
||||
# Deploy the functions
|
||||
Push-Location '.\dist'
|
||||
|
||||
# Get the Azure function name according to the settings
|
||||
$AzureFunctionName = $AppSettings.Values.Azure_Function_Name
|
||||
|
||||
Write-Output "Deploy to function $AzureFunctionName..."
|
||||
|
||||
if ($OverwriteSettings.IsPresent) {
|
||||
# https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#publish
|
||||
func azure functionapp publish $AzureFunctionName --publish-local-settings --overwrite-settings
|
||||
} else {
|
||||
func azure functionapp publish $AzureFunctionName
|
||||
}
|
||||
|
||||
Pop-Location
|
|
@ -0,0 +1,8 @@
|
|||
<testsuites name="jest tests" tests="2" failures="0" time="6.464">
|
||||
<testsuite name="POST /api/query/enhance" errors="0" failures="0" skipped="0" timestamp="2018-11-11T22:25:37" time="4.132" tests="2">
|
||||
<testcase classname="POST /api/query/enhance should throw a warning message if the input query is empty" name="POST /api/query/enhance should throw a warning message if the input query is empty" time="0.005">
|
||||
</testcase>
|
||||
<testcase classname="POST /api/query/enhance should throw an error if the language is not supported" name="POST /api/query/enhance should throw an error if the language is not supported" time="0.003">
|
||||
</testcase>
|
||||
</testsuite>
|
||||
</testsuites>
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"luis_schema_version": "2.2.0",
|
||||
"versionId": "1.0",
|
||||
"name": "PnP - SharePoint Search Enhancement (en-us)",
|
||||
"desc": "PnP - Search keywords and intents recognition (English)",
|
||||
"culture": "en-us",
|
||||
"intents": [
|
||||
{
|
||||
"name": "None"
|
||||
},
|
||||
{
|
||||
"name": "PnP.SearchByKeywords"
|
||||
}
|
||||
],
|
||||
"entities": [],
|
||||
"composites": [],
|
||||
"closedLists": [
|
||||
],
|
||||
"regex_entities": [
|
||||
],
|
||||
"bing_entities": [],
|
||||
"model_features": [],
|
||||
"regex_features": [],
|
||||
"prebuiltEntities": [
|
||||
{
|
||||
"name": "keyPhrase",
|
||||
"roles": []
|
||||
}
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"pattern": "i'm looking for {keyPhrase}",
|
||||
"intent": "PnP.SearchByKeywords"
|
||||
},
|
||||
{
|
||||
"pattern": "give me infos about {keyPhrase}",
|
||||
"intent": "PnP.SearchByKeywords"
|
||||
},
|
||||
{
|
||||
"pattern": "{keyPhrase}",
|
||||
"intent": "PnP.SearchByKeywords"
|
||||
}
|
||||
],
|
||||
"utterances": [
|
||||
{
|
||||
"text": "sharepoint documents",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
},
|
||||
{
|
||||
"text": "architectural schemas",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
},
|
||||
{
|
||||
"text": "i'm looking for azure resources",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
},
|
||||
{
|
||||
"text": "office 365 governance plan",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"luis_schema_version": "2.2.0",
|
||||
"versionId": "1.0",
|
||||
"name": "PnP - Optimiseur de recherche SharePoint (fr-fr)",
|
||||
"desc": "PnP - Reconnaissance de mots clés de recherche (Français)",
|
||||
"culture": "fr-fr",
|
||||
"intents": [
|
||||
{
|
||||
"name": "None"
|
||||
},
|
||||
{
|
||||
"name": "PnP.SearchByKeywords"
|
||||
}
|
||||
],
|
||||
"entities": [
|
||||
|
||||
],
|
||||
"composites": [],
|
||||
"closedLists": [
|
||||
],
|
||||
"regex_entities": [
|
||||
],
|
||||
"bing_entities": [],
|
||||
"model_features": [],
|
||||
"regex_features": [],
|
||||
"prebuiltEntities": [
|
||||
{
|
||||
"name": "keyPhrase",
|
||||
"roles": []
|
||||
}
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"pattern": "je cherche des informations sur {keyPhrase} et {keyPhrase}",
|
||||
"intent": "PnP.SearchByKeywords"
|
||||
},
|
||||
{
|
||||
"pattern": "{keyPhrase}",
|
||||
"intent": "PnP.SearchByKeywords"
|
||||
},
|
||||
{
|
||||
"pattern": "documents sur {keyPhrase}",
|
||||
"intent": "PnP.SearchByKeywords"
|
||||
}
|
||||
],
|
||||
"utterances": [
|
||||
{
|
||||
"text": "documents sur sharepoint",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
},
|
||||
{
|
||||
"text": "je cherche des ressources sur azure",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
},
|
||||
{
|
||||
"text": "plan de gouvernance office 365",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
},
|
||||
{
|
||||
"text": "schémas d'architecture",
|
||||
"intent": "PnP.SearchByKeywords",
|
||||
"entities": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"name": "pnp-search-query-enhancement",
|
||||
"version": "1.0.0",
|
||||
"description": "Azure Function to enhance SharePoint search queries",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"keywords": [
|
||||
"api",
|
||||
"rest",
|
||||
"azure-functions",
|
||||
"azure"
|
||||
],
|
||||
"author": {
|
||||
"name": "Franck Cornu",
|
||||
"email": "franck.cornu@aequos.ca"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "webpack --config ./tools/build/webpack.prod.js",
|
||||
"build:dev": "webpack --config ./tools/build/webpack.dev.js",
|
||||
"lint": "tslint -p ./tsconfig.json --force",
|
||||
"test": "jest --coverage --colors --verbose",
|
||||
"test:ci": "jest --ci --coverage --colors",
|
||||
"release": "standard-version"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/form-data": "^2.2.1",
|
||||
"@types/lodash": "^4.14.118",
|
||||
"@types/node-fetch": "^1.6.9",
|
||||
"@types/sinon": "^4.3.3",
|
||||
"@types/sprintf-js": "^1.1.1",
|
||||
"azure-functions-ts-essentials": "1.3.1",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"form-data": "^2.3.3",
|
||||
"jest-junit": "^4.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-fetch": "^2.2.1",
|
||||
"path": "^0.12.7",
|
||||
"sinon": "^5.1.1",
|
||||
"sprintf-js": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "22.1.4",
|
||||
"@types/node": "^9.4.7",
|
||||
"awesome-typescript-loader": "~3.4.1",
|
||||
"copy-webpack-plugin": "~4.3.1",
|
||||
"cp-cli": "^1.1.2",
|
||||
"jest": "22.1.4",
|
||||
"standard-version": "~4.3.0",
|
||||
"ts-jest": "22.0.4",
|
||||
"tslint": "~5.9.1",
|
||||
"typescript": "~2.6.2",
|
||||
"uglifyjs-webpack-plugin": "^1.1.8",
|
||||
"webpack": "~3.10.0"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
|
||||
},
|
||||
"testMatch": [
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js"
|
||||
],
|
||||
"testResultsProcessor": "./node_modules/jest-junit",
|
||||
"cache": false,
|
||||
"silent": true,
|
||||
"testURL": "http://localhost/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export interface ILuisMappingsDefinition {
|
||||
apps: Array<{
|
||||
appId: string;
|
||||
language: string;
|
||||
version: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export enum LuisEntities {
|
||||
KeyPhrase = 'builtin.keyPhrase'
|
||||
}
|
||||
|
||||
export enum LuisIntents {
|
||||
SearchByKeywords = 'PnP.SearchByKeywords',
|
||||
None = 'None'
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"apps": [
|
||||
{
|
||||
"appId": "0c49d128-4705-4a37-9e62-c7f16e5f8c8e",
|
||||
"language": "fr",
|
||||
"version": "1.0"
|
||||
},
|
||||
{
|
||||
"appId": "980e7fbb-9438-48c5-ad77-5ea2e1357d4f",
|
||||
"language": "en",
|
||||
"version": "1.0"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
import { Context, HttpMethod, HttpRequest, HttpResponse, HttpStatusCode } from 'azure-functions-ts-essentials';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { LuisHelper } from '../../helpers/LuisHelper';
|
||||
import { TextAnalyticsHelper } from '../../helpers/TextAnalyticsHelper';
|
||||
import { ILuisGetIntentResponse } from '../../models/ILuisGetIntentResponse';
|
||||
import { LuisIntents, ILuisMappingsDefinition } from './config/ILuisMappingsDefinition';
|
||||
import { INlpResponse } from '../../models/INlpResponse';
|
||||
|
||||
/**
|
||||
* Routes the request to the default controller using the relevant method.
|
||||
*/
|
||||
export async function run(context: Context, req: HttpRequest): Promise<HttpResponse> {
|
||||
|
||||
let res: HttpResponse;
|
||||
|
||||
switch (req.method) {
|
||||
case HttpMethod.Get:
|
||||
break;
|
||||
|
||||
case HttpMethod.Post:
|
||||
|
||||
// The user raw query from the search box
|
||||
const rawQuery = req.body
|
||||
? req.body.rawQuery
|
||||
: undefined;
|
||||
|
||||
// The UI language from the front-end component (i.e SPFx)
|
||||
const uiLanguage = req.body
|
||||
? req.body.uiLanguage
|
||||
: undefined;
|
||||
|
||||
// Check if we should use the staging model for LUIS
|
||||
const isStaging = req.body
|
||||
? req.body.isStaging
|
||||
: true;
|
||||
|
||||
const textAnalyticsSubscriptionKey = process.env['TextAnalytics_SubscriptionKey'];
|
||||
const textAnalyticsAzureRegion = process.env['TextAnalytics_AzureRegion'];
|
||||
const luisSubscriptionKey = process.env['LUIS_SubscriptionKey'];
|
||||
const luisAuthoringKey = process.env['LUIS_AuthoringKey'];
|
||||
const luisAzureRegion = process.env['LUIS_AzureRegion'];
|
||||
const bingSpellCheckSubscriptionKey = process.env['Bing_SpellCheckApiKey'];
|
||||
const luisMappingFile = process.env['LUIS_MappingsFile'];
|
||||
|
||||
// Instanciates helpers
|
||||
const textAnalyticsHelper = new TextAnalyticsHelper(textAnalyticsSubscriptionKey, textAnalyticsAzureRegion);
|
||||
const luisHelper = new LuisHelper(luisSubscriptionKey, luisAuthoringKey, luisAzureRegion, bingSpellCheckSubscriptionKey);
|
||||
|
||||
try {
|
||||
|
||||
// Optimize the query
|
||||
const response = await enhanceQuery(
|
||||
rawQuery,
|
||||
uiLanguage,
|
||||
isStaging,
|
||||
luisMappingFile,
|
||||
luisHelper,
|
||||
textAnalyticsHelper);
|
||||
|
||||
res = {
|
||||
status: HttpStatusCode.OK,
|
||||
body: response
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
res = {
|
||||
status: HttpStatusCode.InternalServerError,
|
||||
body: {
|
||||
error: {
|
||||
type: 'function_error',
|
||||
message: error.message
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
case HttpMethod.Patch:
|
||||
break;
|
||||
case HttpMethod.Delete:
|
||||
break;
|
||||
|
||||
default:
|
||||
res = {
|
||||
status: HttpStatusCode.MethodNotAllowed,
|
||||
body: {
|
||||
error: {
|
||||
type: 'not_supported',
|
||||
message: `Method ${req.method} not supported.`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the raw query to an optimzed SharePoint search query using intent recognition an entities extraction
|
||||
* @param rawQuery the user raw query
|
||||
* @param uiLanguage the UI language from the front-end component
|
||||
* @param luisMappingFile the LUIS mappings file path for entities
|
||||
* @param luisHelper the LUIS helper instance
|
||||
* @param textAnalyticsHelper the text analytics helepr instance
|
||||
*/
|
||||
export async function enhanceQuery(
|
||||
rawQuery: string,
|
||||
uiLanguage: string,
|
||||
isStaging: boolean,
|
||||
luisMappingFile: string,
|
||||
luisHelper: LuisHelper,
|
||||
textAnalyticsHelper: TextAnalyticsHelper): Promise<INlpResponse> {
|
||||
|
||||
// Function app JSON default response
|
||||
let response: INlpResponse;
|
||||
|
||||
if (rawQuery) {
|
||||
|
||||
// Default values
|
||||
let debugDetectedLanguage = 'Not recognized (use UI language)';
|
||||
let sharePointsearchQuery = rawQuery;
|
||||
|
||||
// Get LUIS mappings from the configuration file
|
||||
const mappingFile = fs.readFileSync(path.resolve(__dirname, `./${luisMappingFile}`), { encoding: 'utf8' });
|
||||
const mappingsData = JSON.parse(mappingFile.toString()) as ILuisMappingsDefinition;
|
||||
const apps = mappingsData.apps;
|
||||
|
||||
try {
|
||||
|
||||
// Detect the query language
|
||||
let detectedLanguage = await textAnalyticsHelper.detectLanguage(rawQuery);
|
||||
|
||||
// tslint:disable-next-line:curly
|
||||
if (detectedLanguage) {
|
||||
debugDetectedLanguage = detectedLanguage;
|
||||
// tslint:disable-next-line:curly
|
||||
} else {
|
||||
detectedLanguage = uiLanguage;
|
||||
}
|
||||
|
||||
// Select the right LUIS model according to the language
|
||||
const luisModel = apps.filter(app => {
|
||||
return app.language === detectedLanguage;
|
||||
})[0];
|
||||
|
||||
// If there is a LUIS model for this language
|
||||
if (luisModel) {
|
||||
|
||||
luisHelper.appId = luisModel.appId;
|
||||
luisHelper.appVersion = luisModel.version;
|
||||
luisHelper.isStaging = isStaging;
|
||||
|
||||
// Get the top user intent using LUIS
|
||||
const luisResponse: ILuisGetIntentResponse = await luisHelper.getIntentFromQuery(rawQuery);
|
||||
|
||||
// Check if the query has been corrected by the Bing spell checker
|
||||
if (luisResponse.alteredQuery)
|
||||
sharePointsearchQuery = luisResponse.alteredQuery;
|
||||
|
||||
// Get the user intents from LUIS
|
||||
switch (luisResponse.topScoringIntent.intent) {
|
||||
|
||||
case LuisIntents.SearchByKeywords:
|
||||
|
||||
// Get only recognized entities values
|
||||
// Do here whatever you want with your custom entities and intents
|
||||
// For this sample, only builtin entities are retrieved
|
||||
sharePointsearchQuery = luisResponse.entities.map((e) => {
|
||||
return e.entity;
|
||||
}).join(" ");
|
||||
|
||||
break;
|
||||
|
||||
case LuisIntents.None:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Build the response
|
||||
response = {
|
||||
alteredQuery: luisResponse.alteredQuery,
|
||||
topScoringIntent: {
|
||||
detectedIntent: luisResponse.topScoringIntent.intent,
|
||||
confidence: luisResponse.topScoringIntent.score,
|
||||
},
|
||||
detectedLanguage : debugDetectedLanguage,
|
||||
entities: luisResponse.entities,
|
||||
enhancedQuery: sharePointsearchQuery
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:curly
|
||||
} else {
|
||||
throw new Error(`The language '${detectedLanguage}' is not supported by this method`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:curly
|
||||
} else {
|
||||
throw new Error("You can't submit an empty query!");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"disabled": false,
|
||||
"bindings": [
|
||||
{
|
||||
"authLevel": "function",
|
||||
"type": "httpTrigger",
|
||||
"direction": "in",
|
||||
"name": "req",
|
||||
"methods": [
|
||||
"post"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "$return",
|
||||
"type": "http",
|
||||
"direction": "out"
|
||||
}
|
||||
],
|
||||
"scriptFile": "enhanceQuery.js"
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { Context, HttpMethod, HttpRequest, HttpStatusCode } from 'azure-functions-ts-essentials';
|
||||
import { TextAnalyticsHelper } from '../../../helpers/TextAnalyticsHelper';
|
||||
import { run } from '../enhanceQuery';
|
||||
import * as $ from '../../../../tools/build/helpers';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Build the process.env object according to Azure Function Application Settings
|
||||
const localSettingsFile = $.root('./src/local.settings.json');
|
||||
const settings = JSON.parse(fs.readFileSync(localSettingsFile, { encoding: 'utf8' })
|
||||
.toString()).Values;
|
||||
|
||||
Object.keys(settings)
|
||||
.map(key => {
|
||||
process.env[key] = settings[key];
|
||||
});
|
||||
|
||||
describe('POST /api/query/enhance', () => {
|
||||
|
||||
it('should throw a warning message if the input query is empty', async () => {
|
||||
|
||||
const mockContext: Context = {
|
||||
done: (err, response) => {
|
||||
expect(err).toBeUndefined();
|
||||
expect(response.status).toEqual(HttpStatusCode.InternalServerError);
|
||||
expect(response.body.error.message).toBeDefined();
|
||||
}
|
||||
};
|
||||
|
||||
const mockRequest: HttpRequest = {
|
||||
method: HttpMethod.Post,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: {
|
||||
rawQuery: ''
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await run(mockContext, mockRequest);
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw an error if the language is not supported', async () => {
|
||||
|
||||
TextAnalyticsHelper.prototype.detectLanguage = jest.fn().mockReturnValue('la');
|
||||
|
||||
const mockContext: Context = {
|
||||
done: (err, response) => {
|
||||
expect(err).toBeUndefined();
|
||||
expect(response.status).toEqual(HttpStatusCode.InternalServerError);
|
||||
expect(response.body.error.message).toBeDefined();
|
||||
}
|
||||
};
|
||||
|
||||
const mockRequest: HttpRequest = {
|
||||
method: HttpMethod.Post,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: {
|
||||
rawQuery: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await run(mockContext, mockRequest);
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
import * as _ from 'lodash';
|
||||
import fetch, { RequestInit } from 'node-fetch';
|
||||
import { ILuisGetIntentResponse } from '../models/ILuisGetIntentResponse';
|
||||
import { Utilities } from './Utilities';
|
||||
import * as format from 'sprintf-js';
|
||||
|
||||
export class LuisHelper {
|
||||
|
||||
readonly BASE_URL = 'https://%s.api.cognitive.microsoft.com';
|
||||
|
||||
private _subscriptionKey: string;
|
||||
private _authoringKey: string;
|
||||
private _baseUrl: string;
|
||||
private _appId: string;
|
||||
private _appVersion: string;
|
||||
private _isStaging: boolean;
|
||||
private _bingSpellCheckerSubscriptionKey: string;
|
||||
|
||||
get appId(): string {
|
||||
return this._appId;
|
||||
}
|
||||
|
||||
set appId(value: string) {
|
||||
this._appId = value;
|
||||
}
|
||||
|
||||
get appVersion(): string {
|
||||
return this._appVersion;
|
||||
}
|
||||
|
||||
set appVersion(value: string) {
|
||||
this._appVersion = value;
|
||||
}
|
||||
|
||||
get isStaging(): boolean {
|
||||
return this._isStaging;
|
||||
}
|
||||
|
||||
set isStaging(value: boolean) {
|
||||
this._isStaging = value;
|
||||
}
|
||||
|
||||
constructor(subscriptionKey: string, authoringKey: string, azureRegion: string, bingSpellCheckerSubscriptionKey?: string) {
|
||||
|
||||
this._subscriptionKey = subscriptionKey;
|
||||
this._authoringKey = authoringKey;
|
||||
this._baseUrl = format.sprintf(this.BASE_URL, azureRegion);
|
||||
|
||||
this._bingSpellCheckerSubscriptionKey = bingSpellCheckerSubscriptionKey ? bingSpellCheckerSubscriptionKey : undefined;
|
||||
}
|
||||
|
||||
async getIntentFromQuery(query: string): Promise<ILuisGetIntentResponse> {
|
||||
|
||||
const request: RequestInit = {
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': this._subscriptionKey,
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(`"${query}"`)
|
||||
};
|
||||
|
||||
let url = `${this._baseUrl}/luis/v2.0/apps/${this._appId}`;
|
||||
|
||||
if (this._isStaging)
|
||||
url = Utilities.addOrReplaceQueryStringParam(url, 'staging', 'true');
|
||||
|
||||
if (this._bingSpellCheckerSubscriptionKey) {
|
||||
url = Utilities.addOrReplaceQueryStringParam(url, 'spellCheck', 'true');
|
||||
url = Utilities.addOrReplaceQueryStringParam(url, 'bing-spell-check-subscription-key', this._bingSpellCheckerSubscriptionKey);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, request);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
} else {
|
||||
const json = await response.json();
|
||||
|
||||
if (json.errors)
|
||||
throw new Error(json.errors);
|
||||
|
||||
return json as ILuisGetIntentResponse;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import fetch, { RequestInit } from 'node-fetch';
|
||||
import * as format from 'sprintf-js';
|
||||
|
||||
export class TextAnalyticsHelper {
|
||||
|
||||
readonly BASE_URL = 'https://%s.api.cognitive.microsoft.com';
|
||||
readonly SCORE_THRESHOLD = 0.70;
|
||||
|
||||
private _subscriptionKey: string;
|
||||
private _baseUrl: string;
|
||||
|
||||
constructor(subscriptionKey: string, azureRegion: string) {
|
||||
|
||||
this._subscriptionKey = subscriptionKey;
|
||||
this._baseUrl = format.sprintf(this.BASE_URL, azureRegion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the input query language using the Microsoft Text Analytics Service
|
||||
* @param query the query to analyze
|
||||
*/
|
||||
async detectLanguage(query: string): Promise<string> {
|
||||
|
||||
const request: RequestInit = {
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': this._subscriptionKey,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(
|
||||
{
|
||||
documents: [{
|
||||
id: 1,
|
||||
text: query
|
||||
}]
|
||||
})
|
||||
};
|
||||
|
||||
const url = `${this._baseUrl}/text/analytics/v2.0/languages`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, request);
|
||||
const json = await response.json();
|
||||
|
||||
// tslint:disable-next-line:curly
|
||||
if (json.statusCode) {
|
||||
throw new Error(json.message);
|
||||
} else {
|
||||
// Get only language with confidence geater than 70%
|
||||
const isoLanguageName = json.documents[0].detectedLanguages.filter(language => {
|
||||
return language.score > this.SCORE_THRESHOLD;
|
||||
});
|
||||
|
||||
return isoLanguageName.length > 0 ? isoLanguageName[0].iso6391Name : undefined;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
export class Utilities {
|
||||
|
||||
/**
|
||||
* Add or replace a query string parameter
|
||||
* @param url The current URL
|
||||
* @param param The query string parameter to add or replace
|
||||
* @param value The new value
|
||||
*/
|
||||
static addOrReplaceQueryStringParam(url: string, param: string, value: string): string {
|
||||
// tslint:disable-next-line:prefer-template
|
||||
const re = new RegExp('[\\?&]' + param + '=([^&#]*)');
|
||||
const match = re.exec(url);
|
||||
let delimiter;
|
||||
let newString;
|
||||
|
||||
if (match === null) {
|
||||
// Append new param
|
||||
const hasQuestionMark = /\?/.test(url);
|
||||
delimiter = hasQuestionMark ? '&' : '?';
|
||||
// tslint:disable-next-line:prefer-template
|
||||
newString = url + delimiter + param + '=' + value;
|
||||
} else {
|
||||
delimiter = match[0].charAt(0);
|
||||
// tslint:disable-next-line:prefer-template
|
||||
newString = url.replace(re, delimiter + param + '=' + value);
|
||||
}
|
||||
|
||||
return newString;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"version": "2.0"
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"LUIS_SubscriptionKey": "0065cf294a8244409cf40f1400ed74ab",
|
||||
"LUIS_AzureRegion": "eastus",
|
||||
"LUIS_AppConfigFolderPath": ".\\config",
|
||||
"LUIS_MappingsFile": ".\\config\\luismappings.dev.json",
|
||||
"Bing_SpellCheckApiKey": "c5895173d71242b6b1c20cdb28362549",
|
||||
"TextAnalytics_SubscriptionKey": "5332d75141cb4b2c90365299e5262318",
|
||||
"TextAnalytics_AzureRegion": "canadacentral",
|
||||
"Azure_Function_Name": "pnpsearchfunction",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "node",
|
||||
"NODE_OPTIONS": "--inspect=5858"
|
||||
},
|
||||
"Host": {
|
||||
"LocalHttpPort": 7071,
|
||||
"CORS": "*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { LuisIntents, LuisEntities } from "../functions/enhanceQuery/config/ILuisMappingsDefinition";
|
||||
|
||||
export interface ILuisGetIntentResponse {
|
||||
query: string;
|
||||
entities: Array<ILuisResponseEntity>;
|
||||
alteredQuery?: string;
|
||||
topScoringIntent: ILuisResponseIntent;
|
||||
/**
|
||||
* Intents scores. Only visible in verbose mode
|
||||
*/
|
||||
intents?: Array<ILuisResponseIntent>;
|
||||
}
|
||||
|
||||
export interface ILuisResponseIntent {
|
||||
intent: LuisIntents;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface ILuisResponseEntity {
|
||||
entity: string;
|
||||
type: LuisEntities;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
resolution?: {
|
||||
values: Array<string>;
|
||||
};
|
||||
role?: string;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { ILuisResponseEntity, ILuisResponseIntent } from './ILuisGetIntentResponse';
|
||||
|
||||
export interface INlpResponse {
|
||||
|
||||
/**
|
||||
* The corrected query is applicable.
|
||||
*/
|
||||
alteredQuery: string;
|
||||
|
||||
/**
|
||||
* The detected language of the input query.
|
||||
*/
|
||||
detectedLanguage: string;
|
||||
|
||||
/**
|
||||
* The recognized intent from the query
|
||||
*/
|
||||
topScoringIntent: INlpIntent;
|
||||
|
||||
/**
|
||||
* The list of entities recognized in the query.
|
||||
*/
|
||||
entities: Array<ILuisResponseEntity>;
|
||||
|
||||
/**
|
||||
* The final transformed query.
|
||||
*/
|
||||
enhancedQuery: string;
|
||||
}
|
||||
|
||||
export interface INlpIntent {
|
||||
detectedIntent: string;
|
||||
confidence: number;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/proxies",
|
||||
"proxies": {
|
||||
"Optimize": {
|
||||
"matchCondition": {
|
||||
"route": "/api/query/enhance",
|
||||
"methods": [
|
||||
"POST"
|
||||
]
|
||||
},
|
||||
"backendUri": "http://%WEBSITE_HOSTNAME%/api/enhanceQuery"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const path = require('path');
|
||||
|
||||
exports.root = function(args) {
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
args = Array.prototype.slice.call(arguments, 0);
|
||||
|
||||
return path.join.apply(path, [ROOT].concat(args));
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
const $ = require('./helpers');
|
||||
const copyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const cleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
target: 'node',
|
||||
entry: {
|
||||
'enhanceQuery': $.root('./src/functions/enhanceQuery/enhanceQuery.ts')
|
||||
/* 'anotherFunctionEntryPoint': $.root('./src/functions/anotherFunctionEntryPoint/anotherFunctionEntryPoint.ts'),*/
|
||||
},
|
||||
output: {
|
||||
path: $.root('dist'),
|
||||
filename: '[name]/[name].js',
|
||||
libraryTarget: 'commonjs2',
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: 'awesome-typescript-loader?declaration=false',
|
||||
exclude: [/\.(spec|e2e)\.ts$/]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
modules: [
|
||||
'node_modules',
|
||||
'src'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new copyWebpackPlugin([
|
||||
{
|
||||
from: 'src/host.json',
|
||||
to: 'host.json'
|
||||
},
|
||||
{
|
||||
from: 'src/proxies.json',
|
||||
to: 'proxies.json'
|
||||
},
|
||||
{
|
||||
from: 'src/local.settings.json',
|
||||
to: 'local.settings.json'
|
||||
},
|
||||
{
|
||||
context: 'src/functions',
|
||||
from: '**/function.json',
|
||||
to: ''
|
||||
},
|
||||
{
|
||||
context: 'src/functions',
|
||||
from: '**/config/*.json',
|
||||
to: ''
|
||||
}
|
||||
]),
|
||||
new cleanWebpackPlugin(['dist/**/*'], {
|
||||
allowExternal: true,
|
||||
root: $.root('.'),
|
||||
verbose: false
|
||||
}),
|
||||
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/)
|
||||
],
|
||||
node: {
|
||||
__filename: false,
|
||||
__dirname: false,
|
||||
}
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
const $ = require('./helpers');
|
||||
const uglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
const copyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const cleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
target: 'node',
|
||||
entry: {
|
||||
'enhanceQuery': $.root('./src/functions/enhanceQuery/enhanceQuery.ts')
|
||||
/* 'anotherFunctionEntryPoint': $.root('./src/functions/anotherFunctionEntryPoint/anotherFunctionEntryPoint.ts'),*/
|
||||
},
|
||||
output: {
|
||||
path: $.root('dist'),
|
||||
filename: '[name]/[name].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: 'awesome-typescript-loader?declaration=false',
|
||||
exclude: [/\.(spec|e2e)\.ts$/]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
modules: [
|
||||
'node_modules',
|
||||
'src'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new uglifyJSPlugin({
|
||||
uglifyOptions: {
|
||||
ecma: 6
|
||||
}
|
||||
}),
|
||||
new copyWebpackPlugin([
|
||||
{
|
||||
from: 'src/host.json',
|
||||
to: 'host.json'
|
||||
},
|
||||
{
|
||||
from: 'src/proxies.json',
|
||||
to: 'proxies.json'
|
||||
},
|
||||
{
|
||||
context: 'src/functions',
|
||||
from: '**/function.json',
|
||||
to: ''
|
||||
},
|
||||
{
|
||||
from: 'src/local.settings.json',
|
||||
to: 'local.settings.json'
|
||||
},
|
||||
{
|
||||
context: 'src/functions',
|
||||
from: '**/config/*.json',
|
||||
to: ''
|
||||
}
|
||||
]),
|
||||
new cleanWebpackPlugin(['dist/**/*'], {
|
||||
allowExternal: true,
|
||||
root: $.root('.'),
|
||||
verbose: false
|
||||
}),
|
||||
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/)
|
||||
],
|
||||
node: {
|
||||
__filename: false,
|
||||
__dirname: false,
|
||||
}
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"lib": [ "es2015"],
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
"usePrecompiledFiles": true,
|
||||
"useWebpackText": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
{
|
||||
"rules": {
|
||||
"no-unnecessary-class": [
|
||||
true,
|
||||
"allow-constructor-only",
|
||||
"allow-static-only",
|
||||
"allow-empty-class"
|
||||
],
|
||||
"member-access": [
|
||||
true,
|
||||
"no-public"
|
||||
],
|
||||
"member-ordering": [
|
||||
true,
|
||||
"public-before-private",
|
||||
"static-before-instance",
|
||||
"variables-before-functions"
|
||||
],
|
||||
"adjacent-overload-signatures": true,
|
||||
"unified-signatures": true,
|
||||
"prefer-function-over-method": [
|
||||
true,
|
||||
"allow-public",
|
||||
"allow-protected"
|
||||
],
|
||||
"no-invalid-this": [
|
||||
true,
|
||||
"check-function-in-method"
|
||||
],
|
||||
"no-duplicate-super": true,
|
||||
"new-parens": true,
|
||||
"no-misused-new": true,
|
||||
"no-construct": true,
|
||||
"no-empty-interface": true,
|
||||
"prefer-method-signature": true,
|
||||
"interface-over-type-literal": true,
|
||||
"no-arg": true,
|
||||
"only-arrow-functions": [
|
||||
true,
|
||||
"allow-declarations",
|
||||
"allow-named-functions"
|
||||
],
|
||||
"arrow-parens": [
|
||||
true,
|
||||
"ban-single-arg-parens"
|
||||
],
|
||||
"arrow-return-shorthand": true,
|
||||
"no-return-await": true,
|
||||
"prefer-const": true,
|
||||
"no-shadowed-variable": [
|
||||
true,
|
||||
{
|
||||
"temporalDeadZone": false
|
||||
}
|
||||
],
|
||||
"one-variable-per-declaration": [
|
||||
true,
|
||||
"ignore-for-loop"
|
||||
],
|
||||
"no-duplicate-variable": [
|
||||
true,
|
||||
"check-parameters"
|
||||
],
|
||||
"no-unnecessary-initializer": true,
|
||||
"no-implicit-dependencies": true,
|
||||
"ordered-imports": [
|
||||
true,
|
||||
{
|
||||
"import-sources-order": "any",
|
||||
"named-imports-order": "case-insensitive",
|
||||
"grouped-imports": true
|
||||
}
|
||||
],
|
||||
"no-duplicate-imports": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs"
|
||||
],
|
||||
"no-require-imports": true,
|
||||
"no-default-export": true,
|
||||
"no-reference": true,
|
||||
"typedef": [
|
||||
true,
|
||||
"call-signature",
|
||||
"property-declaration"
|
||||
],
|
||||
"no-inferrable-types": true,
|
||||
"no-angle-bracket-type-assertion": true,
|
||||
"callable-types": true,
|
||||
"no-null-keyword": true,
|
||||
"no-non-null-assertion": true,
|
||||
"array-type": [
|
||||
true,
|
||||
"generic"
|
||||
],
|
||||
"prefer-object-spread": true,
|
||||
"object-literal-shorthand": true,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-template",
|
||||
"avoid-escape"
|
||||
],
|
||||
"prefer-template": true,
|
||||
"no-invalid-template-strings": true,
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"binary-expression-operand-order": true,
|
||||
"no-dynamic-delete": true,
|
||||
"no-bitwise": true,
|
||||
"use-isnan": true,
|
||||
"no-conditional-assignment": true,
|
||||
"prefer-conditional-expression": [
|
||||
true,
|
||||
"check-else-if"
|
||||
],
|
||||
"prefer-for-of": true,
|
||||
"forin": true,
|
||||
"switch-default": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unsafe-finally": true,
|
||||
"no-duplicate-switch-case": true,
|
||||
"encoding": true,
|
||||
"cyclomatic-complexity": [
|
||||
true,
|
||||
20
|
||||
],
|
||||
"max-file-line-count": [
|
||||
true,
|
||||
1000
|
||||
],
|
||||
"max-line-length": [
|
||||
true,
|
||||
300
|
||||
],
|
||||
"indent": [
|
||||
true,
|
||||
"spaces",
|
||||
2
|
||||
],
|
||||
"eofline": true,
|
||||
"curly": [
|
||||
true,
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-module",
|
||||
"check-separator",
|
||||
"check-rest-spread",
|
||||
"check-type",
|
||||
"check-typecast",
|
||||
"check-type-operator",
|
||||
"check-preblock"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
],
|
||||
"space-before-function-paren": [
|
||||
true,
|
||||
{
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
"method": "never",
|
||||
"constructor": "never"
|
||||
}
|
||||
],
|
||||
"space-within-parens": 0,
|
||||
"import-spacing": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"newline-before-return": true,
|
||||
"newline-per-chained-call": true,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace",
|
||||
"check-else",
|
||||
"check-catch",
|
||||
"check-finally"
|
||||
],
|
||||
"no-consecutive-blank-lines": [
|
||||
true,
|
||||
1
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always",
|
||||
"strict-bound-class-methods"
|
||||
],
|
||||
"align": [
|
||||
true,
|
||||
"parameters",
|
||||
"statements"
|
||||
],
|
||||
"trailing-comma": [
|
||||
true,
|
||||
{
|
||||
"multiline": "never",
|
||||
"singleline": "never",
|
||||
"esSpecCompliant": true
|
||||
}
|
||||
],
|
||||
"class-name": true,
|
||||
"variable-name": [
|
||||
true,
|
||||
"check-format",
|
||||
"allow-leading-underscore",
|
||||
"ban-keywords"
|
||||
],
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"jsdoc-format": [
|
||||
true,
|
||||
"check-multiline-start"
|
||||
],
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-console": [
|
||||
true,
|
||||
"log",
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-debugger": true,
|
||||
"no-eval": true,
|
||||
"no-string-throw": true,
|
||||
"no-namespace": true,
|
||||
"no-internal-module": true,
|
||||
"radix": true,
|
||||
"no-unused-expression": [
|
||||
true,
|
||||
"allow-fast-null-checks"
|
||||
],
|
||||
"no-empty": true,
|
||||
"no-sparse-arrays": true
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 7.5 MiB After Width: | Height: | Size: 7.8 MiB |
After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 52 KiB |
|
@ -1,61 +0,0 @@
|
|||
{
|
||||
"name": "react-search-refiners",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/decorators": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-application-base": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-core-library": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-dialog": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-loader": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-lodash-subset": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-webpart-base": "1.6.0-plusbeta",
|
||||
"@pnp/common": "^1.2.5",
|
||||
"@pnp/logging": "^1.2.5",
|
||||
"@pnp/odata": "^1.2.5",
|
||||
"@pnp/polyfill-ie11": "^1.0.0",
|
||||
"@pnp/sp": "^1.2.5",
|
||||
"@pnp/spfx-controls-react": "^1.10.0",
|
||||
"@pnp/spfx-property-controls": "1.11.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/fabric": "^1.5.45",
|
||||
"@types/handlebars": "^4.0.39",
|
||||
"@types/react": "15.6.6",
|
||||
"@types/react-dom": "15.5.6",
|
||||
"@types/sharepoint": "2013.1.9",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"common-tags": "^1.8.0",
|
||||
"downshift": "1.31.14",
|
||||
"handlebars": "^4.0.12",
|
||||
"handlebars-helpers": "^0.8.4",
|
||||
"immutability-helper": "2.4.0",
|
||||
"office-ui-fabric-react": "^5.131.2",
|
||||
"on-el-resize": "0.0.4",
|
||||
"react": "15.6.2",
|
||||
"react-ace": "6.1.4",
|
||||
"react-custom-scrollbars": "4.1.2",
|
||||
"react-dom": "15.6.2",
|
||||
"react-js-pagination": "3.0.0",
|
||||
"video.js": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-module-interfaces": "1.6.0-plusbeta",
|
||||
"@microsoft/sp-webpart-workbench": "1.6.0-plusbeta",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "^3.9.1",
|
||||
"unlazy-loader": "0.1.3",
|
||||
"webpack-bundle-analyzer": "^2.13.1"
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
|
||||
<CustomAction
|
||||
Title="QueryStringDataSource"
|
||||
Location="ClientSideExtension.ApplicationCustomizer"
|
||||
ClientSideComponentId="24cae67d-dec7-4eff-bb41-49451d5b5a11"
|
||||
ClientSideComponentProperties="{"testMessage":"Test message"}">
|
||||
</CustomAction>
|
||||
</Elements>
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
/**
|
||||
* Install Chrome Debugger Extension for Visual Studio Code to debug your components with the
|
||||
* Chrome browser: https://aka.ms/spfx-debugger-extensions
|
||||
*/
|
||||
"version": "0.2.0",
|
||||
"configurations": [{
|
||||
"name": "Local workbench",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://localhost:4321/temp/workbench.html",
|
||||
"webRoot": "${workspaceRoot}",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../../../../../src/*": "${webRoot}/src/*"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9222"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hosted workbench",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "https://collaborationcorner.sharepoint.com/teams/SharePointAADTokenProvider/_layouts/workbench.aspx",
|
||||
"webRoot": "${workspaceRoot}",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../src/*": "${webRoot}/src/*",
|
||||
"webpack:///../../../../../src/*": "${webRoot}/src/*"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9222",
|
||||
"-incognito"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": false,
|
||||
"environment": "spo",
|
||||
"version": "1.7.0",
|
||||
"libraryName": "react-search-refiners",
|
||||
"libraryId": "ec742d56-6535-4ef1-85e0-27e2c79a9fb5",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"search-results-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/searchResults/SearchResultsWebPart.js",
|
||||
"manifest": "./src/webparts/searchResults/SearchResultsWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"search-box-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/searchBox/SearchBoxWebPart.js",
|
||||
"manifest": "./src/webparts/searchBox/SearchBoxWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"SearchResultsWebPartStrings": "lib/webparts/searchResults/loc/{locale}.js",
|
||||
"SearchBoxWebPartStrings": "lib/webparts/searchBox/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-search-refiners",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "PnP - Search Web Parts",
|
||||
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"version": "2.2.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": false,
|
||||
"isDomainIsolated": false
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/pnp-react-search-refiners.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -45,6 +45,6 @@ build.configureWebpack.mergeConfig({
|
|||
}
|
||||
});
|
||||
build.webpack.buildConfig
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
build.addSuppression(new RegExp("\[sass\]",'g'));
|
||||
|
||||
build.initialize(gulp);
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "pnp-react-search-refiners",
|
||||
"version": "2.2.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "16.3.2",
|
||||
"react-dom": "16.3.2",
|
||||
"@types/react": "16.4.2",
|
||||
"@types/react-dom": "16.0.5",
|
||||
"@microsoft/sp-core-library": "1.7.0",
|
||||
"@microsoft/sp-webpart-base": "1.7.0",
|
||||
"@microsoft/sp-lodash-subset": "1.7.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.7.0",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@microsoft/decorators": "1.7.0",
|
||||
"@microsoft/sp-application-base": "1.7.0",
|
||||
"@microsoft/sp-dialog": "1.7.0",
|
||||
"@microsoft/sp-loader": "1.7.0",
|
||||
"@pnp/common": "1.2.5",
|
||||
"@pnp/logging": "1.2.5",
|
||||
"@pnp/odata": "1.2.5",
|
||||
"@pnp/sp": "1.2.5",
|
||||
"@pnp/polyfill-ie11": "1.0.0",
|
||||
"@pnp/spfx-controls-react": "1.10.0",
|
||||
"@pnp/spfx-property-controls": "1.11.0",
|
||||
"@types/fabric": "^1.5.43",
|
||||
"@types/handlebars": "^4.0.39",
|
||||
"@types/sharepoint": "2013.1.9",
|
||||
"common-tags": "^1.8.0",
|
||||
"downshift": "3.1.5",
|
||||
"handlebars": "^4.0.12",
|
||||
"lodash": "4.17.11",
|
||||
"handlebars-helpers": "^0.8.4",
|
||||
"immutability-helper": "2.4.0",
|
||||
"office-ui-fabric-react": "5.120.0",
|
||||
"on-el-resize": "0.0.4",
|
||||
"react-ace": "6.1.4",
|
||||
"react-custom-scrollbars": "4.1.2",
|
||||
"react-js-pagination": "3.0.0",
|
||||
"video.js": "^7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.7.0",
|
||||
"@microsoft/sp-tslint-rules": "1.7.0",
|
||||
"@microsoft/sp-module-interfaces": "1.7.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.7.0",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
"unlazy-loader": "0.1.3",
|
||||
"webpack-bundle-analyzer": "^2.13.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { HttpClient, HttpClientResponse } from "@microsoft/sp-http";
|
||||
|
||||
class ServiceHelper {
|
||||
|
||||
private _httpClient: HttpClient;
|
||||
|
||||
constructor(httpClient: HttpClient) {
|
||||
|
||||
this._httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures an URL can be resolved via a GET or POST request (i.e not 404)
|
||||
* @param url The URL to test
|
||||
*/
|
||||
public async ensureUrlResovles(url: string): Promise<void> {
|
||||
try {
|
||||
const responseGet: HttpClientResponse = await this._httpClient.get(url, HttpClient.configurations.v1);
|
||||
const responsePost = await this._httpClient.post(url, HttpClient.configurations.v1, {});
|
||||
|
||||
if ((responseGet.status !== 404) || (responsePost.status !== 404)) {
|
||||
return;
|
||||
} else {
|
||||
throw "Not Found (404)";
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ServiceHelper;
|
|
@ -0,0 +1,19 @@
|
|||
interface INlpRequest {
|
||||
|
||||
/**
|
||||
* The raw query from the user in the search box
|
||||
*/
|
||||
rawQuery: string;
|
||||
|
||||
/**
|
||||
* The current UI language. Used to determine the language for optimization
|
||||
*/
|
||||
uiLanguage: string;
|
||||
|
||||
/**
|
||||
* Indicates if we should use the LUIS staging model for optimization
|
||||
*/
|
||||
isStaging: boolean;
|
||||
}
|
||||
|
||||
export default INlpRequest;
|
|
@ -0,0 +1,52 @@
|
|||
export interface INlpResponse {
|
||||
|
||||
/**
|
||||
* The detected language of the query ('fr', 'en', etc.)
|
||||
*/
|
||||
detectedLanguage: string;
|
||||
|
||||
/**
|
||||
* The corrected query if the original query had grammar syntax mistakes
|
||||
*/
|
||||
alteredQuery?: string;
|
||||
|
||||
/**
|
||||
* The recognized intent from the query
|
||||
*/
|
||||
topScoringIntent: NlpIntent;
|
||||
|
||||
/**
|
||||
* Recognized entities in the query
|
||||
*/
|
||||
entities: INlpEntity[];
|
||||
|
||||
/**
|
||||
* The resulting SharePoint search query
|
||||
*/
|
||||
enhancedQuery: string;
|
||||
}
|
||||
|
||||
export interface INlpEntity {
|
||||
|
||||
/**
|
||||
* The LUIS entitiy name
|
||||
*/
|
||||
entity: string;
|
||||
|
||||
/**
|
||||
* Type of the entity
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* All resolutions for the entitiy (i.e. recognized occurences)
|
||||
*/
|
||||
resolution: {
|
||||
values: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface NlpIntent {
|
||||
detectedIntent: string;
|
||||
confidence: number;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
interface ISearchQuery {
|
||||
|
||||
/**
|
||||
* The search query as it appears in the search box input
|
||||
*/
|
||||
rawInputValue: string;
|
||||
|
||||
/**
|
||||
* The enhanced query retreived from NLP service
|
||||
*/
|
||||
enhancedQuery: string;
|
||||
}
|
||||
|
||||
export default ISearchQuery;
|
|
@ -0,0 +1,13 @@
|
|||
import { INlpResponse } from "../../models/INlpResponse";
|
||||
|
||||
interface INlpService {
|
||||
|
||||
/**
|
||||
* Interprets the user search query intents and return the relevant keywords
|
||||
* @param rawQuery the user raw query input
|
||||
* @param isStaging indicates if we should use the LUIS staging model
|
||||
*/
|
||||
enhanceSearchQuery(rawQuery: string, isStaging: boolean): Promise<INlpResponse>;
|
||||
}
|
||||
|
||||
export default INlpService;
|
|
@ -0,0 +1,50 @@
|
|||
import INlpService from "./INlpService";
|
||||
import { INlpResponse } from "../../models/INlpResponse";
|
||||
|
||||
class MockNlpService implements INlpService {
|
||||
|
||||
private _enhancedQueryData: INlpResponse;
|
||||
|
||||
constructor() {
|
||||
// Define mock data
|
||||
this._enhancedQueryData = {
|
||||
alteredQuery: null,
|
||||
detectedLanguage: 'fr',
|
||||
topScoringIntent: {
|
||||
confidence: 0.8,
|
||||
detectedIntent: "Test"
|
||||
},
|
||||
entities: [
|
||||
{
|
||||
entity: "assurance maladie",
|
||||
resolution: {
|
||||
values: [
|
||||
"b261b287-750e-4699-8472-4ab04ab7601e"
|
||||
]
|
||||
},
|
||||
type:"BNCSearch.NormalizedSubject"
|
||||
},
|
||||
{
|
||||
entity: "maladie",
|
||||
resolution: {
|
||||
values: [
|
||||
"d003c2ff-0352-41d8-8890-b7825352fd83"
|
||||
]
|
||||
},
|
||||
type: "BNCSearch.NormalizedSubject"
|
||||
}
|
||||
],
|
||||
enhancedQuery: `((assurance maladie maladie) OR ("b261b287-750e-4699-8472-4ab04ab7601e" OR "d003c2ff-0352-41d8-8890-b7825352fd83") XRANK(cb=500) (owstaxIdBNCSubject:L0|#b261b287-750e-4699-8472-4ab04ab7601e OR owstaxIdBNCSubject:L0|#d003c2ff-0352-41d8-8890-b7825352fd83)) XRANK(cb=400) (owstaxIdBNCSubject:GPP|#b261b287-750e-4699-8472-4ab04ab7601e OR owstaxIdBNCSubject:GPP|#d003c2ff-0352-41d8-8890-b7825352fd83)`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interprets the user search query intents and return the optimized SharePoint query counterpart
|
||||
* @param rawQuery the user raw query input
|
||||
*/
|
||||
public async enhanceSearchQuery(rawQuery: string): Promise<INlpResponse> {
|
||||
return this._enhancedQueryData;
|
||||
}
|
||||
}
|
||||
|
||||
export default MockNlpService;
|
|
@ -0,0 +1,65 @@
|
|||
import { HttpClient } from "@microsoft/sp-http";
|
||||
import { IWebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { ConsoleListener, LogLevel, Logger } from '@pnp/logging';
|
||||
import INlpService from "./INlpService";
|
||||
import { INlpResponse } from "../../models/INlpResponse";
|
||||
import INlpRequest from "../../models/INlpRequest";
|
||||
|
||||
class NlpService implements INlpService {
|
||||
|
||||
private _serviceUrl: string;
|
||||
private _spfxContext: IWebPartContext;
|
||||
|
||||
constructor(context: IWebPartContext, serviceUrl: string) {
|
||||
this._serviceUrl = serviceUrl;
|
||||
this._spfxContext = context;
|
||||
|
||||
const consoleListener = new ConsoleListener();
|
||||
Logger.subscribe(consoleListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interprets the user search query intents and return the optimized SharePoint query counterpart
|
||||
* @param rawQuery the user raw query input
|
||||
*/
|
||||
public async enhanceSearchQuery(rawQuery: string, isStaging: boolean): Promise<INlpResponse> {
|
||||
|
||||
const postData: string = JSON.stringify({
|
||||
rawQuery: rawQuery,
|
||||
uiLanguage: this._spfxContext.pageContext.cultureInfo.currentUICultureName.split("-")[0],
|
||||
isStaging: isStaging
|
||||
} as INlpRequest);
|
||||
|
||||
// Make the call to the optimizer service
|
||||
const url = this._serviceUrl;
|
||||
|
||||
const requestHeaders = new Headers();
|
||||
requestHeaders.append('Accept','application/json;');
|
||||
requestHeaders.append('Content-Type','application/json; charset=utf-8');
|
||||
requestHeaders.append('Cache-Control','no-cache');
|
||||
|
||||
try {
|
||||
|
||||
const results = await this._spfxContext.httpClient.post(url, HttpClient.configurations.v1, {
|
||||
headers: requestHeaders,
|
||||
body: postData
|
||||
});
|
||||
|
||||
const response: INlpResponse = await results.json();
|
||||
|
||||
if (results.status === 200) {
|
||||
return response;
|
||||
} else {
|
||||
const error = JSON.stringify(response);
|
||||
Logger.write(`[NlpService.enhanceSearchQuery()]: Error: '${error}' for url '${url}'`, LogLevel.Error);
|
||||
throw new Error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error ? error.message : `Failed to fetch URL '${url}'`;
|
||||
Logger.write(`[NlpService.enhanceSearchQuery()]: Error: '${errorMessage}' for url '${url}'`, LogLevel.Error);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NlpService;
|
|
@ -275,7 +275,7 @@ class SearchService implements ISearchService {
|
|||
|
||||
const searchSuggestQuery: SearchSuggestQuery = {
|
||||
preQuery: true,
|
||||
querytext: query,
|
||||
querytext: encodeURIComponent(query.replace(/'/g, '\'\'')),
|
||||
count: 10,
|
||||
hitHighlighting: true,
|
||||
prefixMatch: true,
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
import ITaxonomyService from './ITaxonomyService';
|
||||
|
||||
class MockTaxonomyDataProvider implements ITaxonomyService {
|
||||
class MockTaxonomyService implements ITaxonomyService {
|
||||
|
||||
public initialize(): Promise<void> {
|
||||
const p1 = new Promise<void>((resolve, reject) => {
|
||||
|
@ -16,4 +16,4 @@ class MockTaxonomyDataProvider implements ITaxonomyService {
|
|||
}
|
||||
}
|
||||
|
||||
export default MockTaxonomyDataProvider;
|
||||
export default MockTaxonomyService;
|
|
@ -1,5 +1,5 @@
|
|||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { Logger, LogLevel, ConsoleListener } from '@pnp/logging';
|
||||
import { Logger, LogLevel } from '@pnp/logging';
|
||||
import { SPComponentLoader } from '@microsoft/sp-loader';
|
||||
import ITaxonomyService from './ITaxonomyService';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
|
@ -5,7 +5,7 @@ import * as Handlebars from 'handlebars';
|
|||
import { ISearchResult } from '../../models/ISearchResult';
|
||||
import { html } from 'common-tags';
|
||||
import { isEmpty, uniqBy, uniq } from '@microsoft/sp-lodash-subset';
|
||||
import * as strings from 'SearchWebPartStrings';
|
||||
import * as strings from 'SearchResultsWebPartStrings';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import 'video.js/dist/video-js.css';
|
||||
import { Logger } from '@pnp/logging';
|
|
@ -1,5 +1,5 @@
|
|||
import { PageOpenBehavior } from '../../helpers/UrlHelper';
|
||||
import IDynamicDataSourceConnection from '../../models/IDynamicDataSourceConnection';
|
||||
import { DynamicProperty } from '@microsoft/sp-component-base';
|
||||
|
||||
interface ISearchBoxWebPartProps {
|
||||
searchInNewPage: boolean;
|
||||
|
@ -7,7 +7,11 @@ interface ISearchBoxWebPartProps {
|
|||
openBehavior: PageOpenBehavior;
|
||||
enableQuerySuggestions: boolean;
|
||||
useDynamicDataSource: boolean;
|
||||
sourceInstance: IDynamicDataSourceConnection;
|
||||
NlpServiceUrl: string;
|
||||
enableNlpService: boolean;
|
||||
enableDebugMode: boolean;
|
||||
isStaging: boolean;
|
||||
defaultQueryKeywords: DynamicProperty<string>;
|
||||
}
|
||||
|
||||
export default ISearchBoxWebPartProps;
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "096b96cc-8a44-41fa-9b4d-c0ab2ab2a779",
|
||||
"alias": "SearchBoxWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
@ -21,9 +21,10 @@
|
|||
"title": {
|
||||
"default": "Search Box" , "fr-fr": "Boîte de recherche"
|
||||
},
|
||||
"description": { "default": "Allows users to enter query keywords", "fr-fr": "Permets aux utilisateurs d'entrer des mots clés de recherche." },
|
||||
"description": { "default": "Allows users to enter query keywords", "fr-fr": "Permets aux utilisateurs d'entrer des mots clés de recherche." },
|
||||
"officeFabricIconFontName": "Search",
|
||||
"properties": {
|
||||
"defaultQueryKeywords": ""
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version, Environment, Text, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
IPropertyPaneField,
|
||||
PropertyPaneCheckbox,
|
||||
PropertyPaneDropdown,
|
||||
PropertyPaneToggle,
|
||||
PropertyPaneLabel,
|
||||
IWebPartPropertiesMetadata,
|
||||
PropertyPaneHorizontalRule,
|
||||
PropertyPaneDynamicFieldSet,
|
||||
PropertyPaneDynamicField,
|
||||
DynamicDataSharedDepth
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import * as strings from 'SearchBoxWebPartStrings';
|
||||
import ISearchBoxWebPartProps from './ISearchBoxWebPartProps';
|
||||
import { IDynamicDataCallables, IDynamicDataPropertyDefinition, IDynamicDataSource } from '@microsoft/sp-dynamic-data';
|
||||
import ISearchQuery from '../../models/ISearchQuery';
|
||||
import { ISearchBoxContainerProps } from './components/ISearchBoxContainerProps';
|
||||
import ServiceHelper from '../../helpers/ServiceHelper';
|
||||
import ISearchService from '../../services/SearchService/ISearchService';
|
||||
import INlpService from '../../services/NlpService/INlpService';
|
||||
import MockSearchService from '../../services/SearchService/MockSearchService';
|
||||
import SearchService from '../../services/SearchService/SearchService';
|
||||
import MockNlpService from '../../services/NlpService/MockNlpService';
|
||||
import NlpService from '../../services/NlpService/NlpService';
|
||||
import { PageOpenBehavior } from '../../helpers/UrlHelper';
|
||||
import SearchBoxContainer from './components/SearchBoxContainer';
|
||||
|
||||
export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWebPartProps> implements IDynamicDataCallables {
|
||||
|
||||
private _searchQuery: ISearchQuery;
|
||||
private _searchService: ISearchService;
|
||||
private _serviceHelper: ServiceHelper;
|
||||
private _nlpService: INlpService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Initialize default values for search query
|
||||
this._searchQuery = {
|
||||
rawInputValue: '',
|
||||
enhancedQuery: ''
|
||||
};
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
|
||||
let inputValue = this.properties.defaultQueryKeywords.tryGetValue();
|
||||
|
||||
if (inputValue && typeof(inputValue) === 'string') {
|
||||
this._searchQuery.rawInputValue = inputValue;
|
||||
}
|
||||
|
||||
const element: React.ReactElement<ISearchBoxContainerProps> = React.createElement(
|
||||
SearchBoxContainer, {
|
||||
onSearch: this._onSearch,
|
||||
searchInNewPage: this.properties.searchInNewPage,
|
||||
pageUrl: this.properties.pageUrl,
|
||||
openBehavior: this.properties.openBehavior,
|
||||
inputValue: this._searchQuery.rawInputValue,
|
||||
enableQuerySuggestions: this.properties.enableQuerySuggestions,
|
||||
searchService: this._searchService,
|
||||
enableDebugMode: this.properties.enableDebugMode,
|
||||
enableNlpService: this.properties.enableNlpService,
|
||||
isStaging: this.properties.isStaging,
|
||||
NlpService: this._nlpService
|
||||
} as ISearchBoxContainerProps);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of dynamic data properties that this dynamic data source
|
||||
* returns
|
||||
*/
|
||||
public getPropertyDefinitions(): ReadonlyArray<IDynamicDataPropertyDefinition> {
|
||||
return [
|
||||
{
|
||||
id: 'searchQuery',
|
||||
title: strings.DynamicData.SearchQueryPropertyLabel
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current value of the specified dynamic data set
|
||||
* @param propertyId ID of the dynamic data set to retrieve the value for
|
||||
*/
|
||||
public getPropertyValue(propertyId: string): any {
|
||||
|
||||
switch (propertyId) {
|
||||
case 'searchQuery':
|
||||
|
||||
let property = {
|
||||
[strings.DynamicData.RawInputValuePropertyLabel]: this._searchQuery.rawInputValue
|
||||
};
|
||||
|
||||
if (this.properties.enableNlpService && this.properties.NlpServiceUrl) {
|
||||
property[strings.DynamicData.EnhancedQueryPropertyLabel] = this._searchQuery.enhancedQuery;
|
||||
}
|
||||
|
||||
return property;
|
||||
|
||||
default:
|
||||
throw new Error('Bad property id');
|
||||
}
|
||||
}
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
|
||||
this._serviceHelper = new ServiceHelper(this.context.httpClient);
|
||||
this.context.dynamicDataSourceManager.initializeSource(this);
|
||||
|
||||
this.initSearchService();
|
||||
this.initNlpService();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.SearchBoxQuerySettings,
|
||||
groupFields: this._getSearchQueryFields()
|
||||
},
|
||||
{
|
||||
groupName: strings.SearchBoxNewPage,
|
||||
groupFields: this._getSearchBehaviorOptionsFields()
|
||||
},
|
||||
{
|
||||
groupName: strings.SearchBoxQueryNlpSettings,
|
||||
groupFields: this._getSearchQueryOptimizationFields()
|
||||
},
|
||||
],
|
||||
displayGroupsAsAccordion: true
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string) {
|
||||
this.initSearchService();
|
||||
this.initNlpService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler used to notify data source subscribers when the input query is updated
|
||||
*/
|
||||
private _onSearch = (searchQuery: ISearchQuery): void => {
|
||||
|
||||
this._searchQuery = searchQuery;
|
||||
this.context.dynamicDataSourceManager.notifyPropertyChanged('searchQuery');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the string is a correct URL
|
||||
* @param value the URL to verify
|
||||
*/
|
||||
private _validatePageUrl(value: string) {
|
||||
|
||||
if ((!/^(https?):\/\/[^\s/$.?#].[^\s]*/.test(value) || !value) && this.properties.searchInNewPage) {
|
||||
return strings.SearchBoxUrlErrorMessage;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the service URL is valid
|
||||
* @param value the service URL
|
||||
*/
|
||||
private async _validateServiceUrl(value: string) {
|
||||
|
||||
if ((!/^(https?):\/\/[^\s/$.?#].[^\s]*/.test(value) || !value)) {
|
||||
return strings.SearchBoxUrlErrorMessage;
|
||||
} else {
|
||||
if (Environment.type !== EnvironmentType.Local) {
|
||||
try {
|
||||
await this._serviceHelper.ensureUrlResovles(value);
|
||||
return '';
|
||||
} catch (errorMessage) {
|
||||
return Text.format(strings.UrlNotResolvedErrorMessage, value, errorMessage);
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the query suggestions data provider instance according to the current environnement
|
||||
*/
|
||||
private initSearchService() {
|
||||
|
||||
if (this.properties.enableQuerySuggestions) {
|
||||
if (Environment.type === EnvironmentType.Local ) {
|
||||
this._searchService = new MockSearchService();
|
||||
} else {
|
||||
this._searchService = new SearchService(this.context);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the query optimization data provider instance according to the current environment
|
||||
*/
|
||||
private initNlpService() {
|
||||
|
||||
if (this.properties.enableNlpService && this.properties.NlpServiceUrl) {
|
||||
if (Environment.type === EnvironmentType.Local) {
|
||||
this._nlpService = new MockNlpService();
|
||||
this.properties.NlpServiceUrl = 'https://localhost:7071/api/example';
|
||||
} else {
|
||||
this._nlpService = new NlpService(this.context, this.properties.NlpServiceUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected get propertiesMetadata(): IWebPartPropertiesMetadata {
|
||||
return {
|
||||
'defaultQueryKeywords': {
|
||||
dynamicPropertyType: 'string'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the group fields for the search query options inside the property pane
|
||||
*/
|
||||
private _getSearchQueryFields(): IPropertyPaneField<any>[] {
|
||||
|
||||
// Sets up search query fields
|
||||
let searchQueryConfigFields: IPropertyPaneField<any>[] = [
|
||||
PropertyPaneCheckbox('useDynamicDataSource', {
|
||||
checked: false,
|
||||
text: strings.DynamicData.UseDynamicDataSourceLabel,
|
||||
})
|
||||
];
|
||||
|
||||
if (this.properties.useDynamicDataSource) {
|
||||
searchQueryConfigFields.push(
|
||||
PropertyPaneDynamicFieldSet({
|
||||
label: strings.DynamicData.DefaultQueryKeywordsPropertyLabel,
|
||||
fields: [
|
||||
PropertyPaneDynamicField('defaultQueryKeywords', {
|
||||
label: strings.DynamicData.DefaultQueryKeywordsPropertyLabel,
|
||||
})
|
||||
],
|
||||
sharedConfiguration: {
|
||||
depth: DynamicDataSharedDepth.Source,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return searchQueryConfigFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the group fields for the search options inside the property pane
|
||||
*/
|
||||
private _getSearchBehaviorOptionsFields(): IPropertyPaneField<any>[] {
|
||||
|
||||
let searchBehaviorOptionsFields: IPropertyPaneField<any>[] = [
|
||||
PropertyPaneToggle("enableQuerySuggestions", {
|
||||
checked: false,
|
||||
label: strings.SearchBoxEnableQuerySuggestions
|
||||
}),
|
||||
PropertyPaneHorizontalRule(),
|
||||
PropertyPaneCheckbox('searchInNewPage', {
|
||||
text: strings.SearchBoxSearchInNewPageLabel
|
||||
})
|
||||
];
|
||||
|
||||
if (this.properties.searchInNewPage) {
|
||||
searchBehaviorOptionsFields = searchBehaviorOptionsFields.concat([
|
||||
PropertyPaneTextField('pageUrl', {
|
||||
disabled: !this.properties.searchInNewPage,
|
||||
label: strings.SearchBoxPageUrlLabel,
|
||||
onGetErrorMessage: this._validatePageUrl.bind(this)
|
||||
}),
|
||||
PropertyPaneDropdown('openBehavior', {
|
||||
label: strings.SearchBoxPageOpenBehaviorLabel,
|
||||
options: [
|
||||
{ key: PageOpenBehavior.Self, text: strings.SearchBoxSameTabOpenBehavior, index: 0 },
|
||||
{ key: PageOpenBehavior.NewTab, text: strings.SearchBoxNewTabOpenBehavior, index: 1 }
|
||||
],
|
||||
disabled: !this.properties.searchInNewPage,
|
||||
selectedKey: 0
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
return searchBehaviorOptionsFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the group fields for the search query optimization inside the property pane
|
||||
*/
|
||||
private _getSearchQueryOptimizationFields(): IPropertyPaneField<any>[] {
|
||||
|
||||
let searchQueryOptimizationFields: IPropertyPaneField<any>[] = [
|
||||
PropertyPaneLabel("", {
|
||||
text: strings.SearchBoxQueryNlpSettingsDescription
|
||||
}),
|
||||
PropertyPaneToggle("enableNlpService", {
|
||||
checked: false,
|
||||
label: strings.SearchBoxUserQueryNlpLabel,
|
||||
})
|
||||
];
|
||||
|
||||
if (this.properties.enableNlpService) {
|
||||
|
||||
searchQueryOptimizationFields.push(
|
||||
PropertyPaneTextField("NlpServiceUrl", {
|
||||
label: strings.SearchBoxServiceUrlLabel,
|
||||
disabled: !this.properties.enableNlpService,
|
||||
onGetErrorMessage: this._validateServiceUrl.bind(this),
|
||||
description: Text.format(strings.SearchBoxServiceUrlDescription, window.location.host)
|
||||
}),
|
||||
PropertyPaneToggle("enableDebugMode", {
|
||||
checked: false,
|
||||
label: strings.SearchBoxUseDebugModeLabel,
|
||||
disabled: !this.properties.enableNlpService,
|
||||
}),
|
||||
PropertyPaneToggle("isStaging", {
|
||||
checked: true,
|
||||
label: strings.SearchBoxUseStagingEndpoint,
|
||||
disabled: !this.properties.enableNlpService,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.properties.enableDebugMode = false;
|
||||
}
|
||||
|
||||
return searchQueryOptimizationFields;
|
||||
}
|
||||
}
|