New sample - SPFx React Textbox.io rich text editor integration (#340)
* Added the react-search-refiners folder * Updated pagination + format date for DateTime filters * Added grouped list in the filter panel + fixed some bugs * Added custom scrollbar style * Added a placeholder when the Web Part is not configured * Cleaned code * Miscellaneous fixes before PR. * Added Web Part sample images * Updated images * react-textboxio - Initial release. Need README * Updated README * Added missing image * Updated README * Updated the crawl property in Web Part metadata fro search indexing * Updated the gif image with better quality
This commit is contained in:
parent
2fc7ee1763
commit
89cd64eadb
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.3.2",
|
||||
"libraryName": "react-search-refiners",
|
||||
"libraryId": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
# SharePoint Framework search with refiners and paging sample
|
||||
|
||||
## Summary
|
||||
This sample shows you how to build user friendly SharePoint search experiences using Office UI fabric tiles, custom refiners and paging.
|
||||
|
||||
<p align="center">
|
||||
<img src="./images/react-search-refiners.gif"/>
|
||||
</p>
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-1.3.0-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https:/dev.office.com/sharepoint)
|
||||
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-search-refiners | Franck Cornu (MVP Office Development at aequos) - Twitter @FranckCornu
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0 | October 17, 2017 | Initial release
|
||||
|
||||
## 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.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- In the command line run:
|
||||
- `npm install`
|
||||
- `gulp serve`
|
||||
|
||||
### Web Part property pane options
|
||||
|
||||
The following settings are available in the Web Part property pane:
|
||||
|
||||
<p align="center">
|
||||
<img src="./images/property_pane.png"/>
|
||||
</p>
|
||||
|
||||
Setting | Description
|
||||
-------|----
|
||||
Search query | The search query in KQL format. 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).
|
||||
Selected properties | The search managed properties to retrieve. You can use these proeprties then in the code like this (`item.property_name`). (See the *Tile.tsx* file) .
|
||||
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.
|
||||
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.
|
||||
Show paging | Indicates whether or not the component should show the paging control at the bottom.
|
||||
|
||||
## Features
|
||||
This Web Part illustrates the following concepts on top of the SharePoint Framework:
|
||||
|
||||
- Build an user friendly search experience on the top of the SharePoint search REST API with paging and refiners using the *sp-pnp-js* library.
|
||||
- Integrate the [@pnp/spfx-property-controls](https://github.com/SharePoint/sp-dev-fx-property-controls) in your solution (*PlaceHolder* control).
|
||||
- Integrate multiple Office UI Fabric components (DocumentCard, Panel, GroupedList, ...) to fit with the native Office 365 theme.
|
||||
- Use the React container component approach inspiring by the [react-todo-basic sample](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-todo-basic).
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-search-refiners" />
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"search-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/search/SearchWebPart.js",
|
||||
"manifest": "./src/webparts/search/SearchWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"SearchWebPartStrings": "lib/webparts/search/loc/{locale}.js",
|
||||
"PropertyControlStrings": "./node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://dev.office.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,11 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "PnP - Search and Refiners Web Part",
|
||||
"id": "890affef-33e0-4d72-bd72-36399e02143b",
|
||||
"version": "1.0.0.1"
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/pnp-react-search-refiners.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://dev.office.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,45 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
|
||||
// Display errors as warnings
|
||||
"displayAsWarning": true,
|
||||
// The TSLint task may have been configured with several custom lint rules
|
||||
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
|
||||
// project). If true, this flag will deactivate any of these rules.
|
||||
"removeExistingRules": true,
|
||||
// When true, the TSLint task is configured with some default TSLint "rules.":
|
||||
"useDefaultConfigAsBase": false,
|
||||
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
|
||||
// which are active, other than the list of rules below.
|
||||
"lintConfig": {
|
||||
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-case": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"valid-typeof": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
build.initialize(gulp);
|
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "react-search-refiners",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "~1.3.0",
|
||||
"@microsoft/sp-lodash-subset": "~1.3.0",
|
||||
"@microsoft/sp-webpart-base": "~1.3.0",
|
||||
"@pnp/spfx-controls-react": "^1.0.0-beta.6",
|
||||
"@pnp/spfx-property-controls": "1.0.0-beta.2",
|
||||
"@types/react": "15.0.38",
|
||||
"@types/react-addons-shallow-compare": "0.14.17",
|
||||
"@types/react-addons-test-utils": "0.14.15",
|
||||
"@types/react-addons-update": "0.14.14",
|
||||
"@types/react-dom": "0.14.18",
|
||||
"@types/webpack-env": ">=1.12.1 <1.14.0",
|
||||
"immutability-helper": "2.4.0",
|
||||
"lodash-es": "4.17.4",
|
||||
"moment": "2.18.1",
|
||||
"office-ui-fabric-react": "4.40.2-hotfix.1",
|
||||
"react": "15.4.2",
|
||||
"react-custom-scrollbars": "4.1.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-js-pagination": "3.0.0",
|
||||
"sp-pnp-js": "3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "~1.3.0",
|
||||
"@microsoft/sp-module-interfaces": "~1.3.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.3.0",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { ISearchResults, IRefinementFilter } from "../models/ISearchResult";
|
||||
|
||||
interface ISearchDataProvider {
|
||||
|
||||
/**
|
||||
* Determines the number of items ot retrieve in search REST requests
|
||||
*/
|
||||
resultsCount: number;
|
||||
selectedProperties: string[];
|
||||
|
||||
/**
|
||||
* Performs a SharePoint search query
|
||||
*/
|
||||
search(kqlQuery: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults>;
|
||||
}
|
||||
|
||||
export default ISearchDataProvider;
|
|
@ -0,0 +1,151 @@
|
|||
import ISearchDataProvider from "./ISearchDataProvider";
|
||||
import { ISearchResults, IRefinementFilter, ISearchResult } from "../models/ISearchResult";
|
||||
import intersection from "lodash-es/intersection";
|
||||
import clone from "lodash-es/clone";
|
||||
|
||||
class MockSearchDataProvider implements ISearchDataProvider {
|
||||
|
||||
public selectedProperties: string[];
|
||||
|
||||
private _itemsCount: number;
|
||||
|
||||
public get resultsCount(): number { return this._itemsCount; }
|
||||
public set resultsCount(value: number) { this._itemsCount = value; }
|
||||
|
||||
private _searchResults: ISearchResults;
|
||||
|
||||
public constructor() {
|
||||
|
||||
this._searchResults = {
|
||||
RelevantResults: [
|
||||
{
|
||||
Title: "Document 1 - Category 1",
|
||||
Url: "http://document1.ca",
|
||||
Created: "2017-07-22T15:38:54.0000000Z",
|
||||
RefinementTokenValues: "ǂǂ446f63756d656e74,ǂǂ45647563617465",
|
||||
ContentCategory: "Document",
|
||||
},
|
||||
{
|
||||
Title: "Document 2 - Category 2",
|
||||
Url: "http://document2.ca",
|
||||
Created: "2017-07-22T15:38:54.0000000Z",
|
||||
RefinementTokenValues: "ǂǂ446f63756d656e74,ǂǂ416476697365",
|
||||
ContentCategory: "Document",
|
||||
},
|
||||
{
|
||||
Title: "Form 1",
|
||||
Url: "http://form1.ca",
|
||||
Created: "2017-07-22T15:38:54.0000000Z",
|
||||
RefinementTokenValues: "ǂǂ466f726d",
|
||||
ContentCategory: "Form",
|
||||
},
|
||||
{
|
||||
Title: "Video 1 - Category 1",
|
||||
Url: "https://www.youtube.com/watch?v=S93e6UU7y9o",
|
||||
Created: "2017-07-22T15:38:54.0000000Z",
|
||||
RefinementTokenValues: "ǂǂ566964656f,ǂǂ45647563617465",
|
||||
ContentCategory: "Video",
|
||||
},
|
||||
{
|
||||
Title: "Video 2 - Category 2",
|
||||
Url: "https://www.youtube.com/watch?v=8Nl_dKVQ1O8",
|
||||
Created: "2017-07-22T15:38:54.0000000Z",
|
||||
RefinementTokenValues: "ǂǂ566964656f,ǂǂ416476697365",
|
||||
ContentCategory: "Video",
|
||||
},
|
||||
],
|
||||
RefinementResults: [
|
||||
{
|
||||
FilterName: "Type",
|
||||
Values: [
|
||||
{
|
||||
RefinementCount: 2,
|
||||
RefinementName: "Document",
|
||||
RefinementToken: "ǂǂ446f63756d656e74",
|
||||
RefinementValue: "Document",
|
||||
},
|
||||
{
|
||||
RefinementCount: 2,
|
||||
RefinementName: "Video",
|
||||
RefinementToken: "ǂǂ566964656f",
|
||||
RefinementValue: "Video",
|
||||
},
|
||||
{
|
||||
RefinementCount: 1,
|
||||
RefinementName: "Form",
|
||||
RefinementToken: "ǂǂ466f726d",
|
||||
RefinementValue: "Form",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
FilterName: "Theme",
|
||||
Values: [
|
||||
{
|
||||
RefinementCount: 2,
|
||||
RefinementName: "Category 1",
|
||||
RefinementToken: "ǂǂ45647563617465",
|
||||
RefinementValue: "Category 1",
|
||||
},
|
||||
{
|
||||
RefinementCount: 2,
|
||||
RefinementName: "Category 2",
|
||||
RefinementToken: "ǂǂ416476697365",
|
||||
RefinementValue: "Category 2",
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
TotalRows: 5,
|
||||
};
|
||||
}
|
||||
|
||||
public search(query: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults> {
|
||||
|
||||
const p1 = new Promise<ISearchResults>((resolve, reject) => {
|
||||
|
||||
const filters: string[] = [];
|
||||
let searchResults = clone(this._searchResults);
|
||||
const filteredResults: ISearchResult[] = [];
|
||||
|
||||
if (refinementFilters.length > 0) {
|
||||
refinementFilters.map((filter) => {
|
||||
filters.push(filter.Value.RefinementToken);
|
||||
});
|
||||
|
||||
searchResults.RelevantResults.map((searchResult) => {
|
||||
const filtered = intersection(filters, searchResult.RefinementTokenValues.split(","));
|
||||
if (filtered.length > 0) {
|
||||
filteredResults.push(searchResult);
|
||||
}
|
||||
});
|
||||
|
||||
searchResults = {
|
||||
RelevantResults: filteredResults,
|
||||
RefinementResults: this._searchResults.RefinementResults,
|
||||
TotalRows: filteredResults.length,
|
||||
};
|
||||
}
|
||||
|
||||
// Return only the specified count
|
||||
searchResults.RelevantResults = this._paginate(searchResults.RelevantResults, this._itemsCount, pageNumber);
|
||||
|
||||
// Simulate an async call
|
||||
setTimeout(() => {
|
||||
resolve(searchResults);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return p1;
|
||||
}
|
||||
|
||||
private _paginate (array, pageSize: number, pageNumber: number) {
|
||||
let basePage = --pageNumber * pageSize;
|
||||
|
||||
return pageNumber < 0 || pageSize < 1 || basePage >= array.length
|
||||
? []
|
||||
: array.slice(basePage, basePage + pageSize );
|
||||
}
|
||||
}
|
||||
|
||||
export default MockSearchDataProvider;
|
|
@ -0,0 +1,277 @@
|
|||
import ISearchDataProvider from "./ISearchDataProvider";
|
||||
import { ISearchResults, ISearchResult, IRefinementResult, IRefinementValue, IRefinementFilter } from "../models/ISearchResult";
|
||||
import pnp, { ConsoleListener, Logger, LogLevel, SearchQuery, SearchQueryBuilder, SearchResults, setup, Web, Sort, SortDirection } from "sp-pnp-js";
|
||||
import { IWebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { Text, JsonUtilities, UrlUtilities } from "@microsoft/sp-core-library";
|
||||
import sortBy from "lodash-es/sortBy";
|
||||
import groupBy from 'lodash-es/groupBy';
|
||||
import mapValues from 'lodash-es/mapValues';
|
||||
import mapKeys from "lodash-es/mapKeys";
|
||||
import * as moment from "moment";
|
||||
|
||||
class SearchDataProvider implements ISearchDataProvider {
|
||||
|
||||
private _resultsCount: number;
|
||||
private _context: IWebPartContext;
|
||||
private _appSearchSettings: SearchQuery;
|
||||
private _selectedProperties: string[];
|
||||
|
||||
public get resultsCount(): number { return this._resultsCount; }
|
||||
public set resultsCount(value: number) { this._resultsCount = value; }
|
||||
|
||||
public set selectedProperties(value: string[]) { this._selectedProperties = value; }
|
||||
public get selectedProperties(): string[] { return this._selectedProperties; }
|
||||
|
||||
public constructor(webPartContext: IWebPartContext) {
|
||||
this._context = webPartContext;
|
||||
|
||||
// Setup the PnP JS instance
|
||||
const consoleListener = new ConsoleListener();
|
||||
Logger.subscribe(consoleListener);
|
||||
|
||||
// To limit the payload size, we set odata=nometadata
|
||||
// We just need to get list items here
|
||||
// We also set the SPFx context accordingly (https://github.com/SharePoint/PnP-JS-Core/wiki/Using-sp-pnp-js-in-SharePoint-Framework)
|
||||
setup({
|
||||
sp: {
|
||||
headers: {
|
||||
Accept: "application/json; odata=nometadata",
|
||||
},
|
||||
},
|
||||
spfxContext: this._context,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a search query against SharePoint
|
||||
* @param query The search query in KQL format
|
||||
* @return The search results
|
||||
*/
|
||||
public async search(query: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults> {
|
||||
|
||||
let searchQuery: SearchQuery = {};
|
||||
let sortedRefiners: string[] = [];
|
||||
|
||||
// Search paging option is one based
|
||||
let page = pageNumber ? pageNumber: 1;
|
||||
|
||||
searchQuery.ClientType = "ContentSearchRegular";
|
||||
|
||||
// To be able to use search query variable according to the current context
|
||||
// http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html
|
||||
searchQuery.QueryTemplate = query ? query : "";;
|
||||
|
||||
searchQuery.RowLimit = this._resultsCount;
|
||||
searchQuery.SelectProperties = this._selectedProperties;
|
||||
searchQuery.TrimDuplicates = false;
|
||||
|
||||
let sortList: Sort[] = [
|
||||
{
|
||||
Property: "Created",
|
||||
Direction: SortDirection.Descending
|
||||
},
|
||||
{
|
||||
Property: "Size",
|
||||
Direction: SortDirection.Ascending
|
||||
}
|
||||
];
|
||||
|
||||
searchQuery.SortList = sortList;
|
||||
|
||||
if (refiners) {
|
||||
// Get the refiners order specified in the property pane
|
||||
sortedRefiners = refiners.split(",");
|
||||
searchQuery.Refiners = refiners ? refiners : "";
|
||||
}
|
||||
|
||||
if (refinementFilters) {
|
||||
if (refinementFilters.length > 0) {
|
||||
searchQuery.RefinementFilters = [this._buildRefinementQueryString(refinementFilters)];
|
||||
}
|
||||
}
|
||||
|
||||
let results: ISearchResults = {
|
||||
RelevantResults : [],
|
||||
RefinementResults: [],
|
||||
TotalRows: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
const r = await pnp.sp.search(searchQuery);
|
||||
|
||||
const allItemsPromises: Promise<any>[] = [];
|
||||
let refinementResults: IRefinementResult[] = [];
|
||||
|
||||
// Need to do this check
|
||||
// More info here: https://github.com/SharePoint/PnP-JS-Core/issues/337
|
||||
if (r.RawSearchResults.PrimaryQueryResult) {
|
||||
|
||||
// Be careful, there was an issue with paging calculation under 2.0.8 version of sp-pnp-js library
|
||||
// More info https://github.com/SharePoint/PnP-JS-Core/issues/535
|
||||
const r2 = await r.getPage(page, this._resultsCount);
|
||||
|
||||
const resultRows = r2.RawSearchResults.PrimaryQueryResult.RelevantResults.Table.Rows;
|
||||
let refinementResultsRows = r2.RawSearchResults.PrimaryQueryResult.RefinementResults;
|
||||
|
||||
const refinementRows = refinementResultsRows ? refinementResultsRows["Refiners"] : [];
|
||||
|
||||
// Map search results
|
||||
resultRows.map((elt) => {
|
||||
|
||||
const p1 = new Promise<ISearchResult>((resolvep1, rejectp1) => {
|
||||
|
||||
// Build item result dynamically
|
||||
// We can't type the response here because search results are by definition too heterogeneous so we treat them as key-value object
|
||||
let result: ISearchResult = {};
|
||||
|
||||
elt.Cells.map((item) => {
|
||||
result[item.Key] = item.Value;
|
||||
});
|
||||
|
||||
// Get the icon source URL
|
||||
this._mapToIcon(result.Filename).then((iconUrl) => {
|
||||
|
||||
result.iconSrc = iconUrl;
|
||||
resolvep1(result);
|
||||
|
||||
}).catch((error) => {
|
||||
rejectp1(error);
|
||||
});
|
||||
});
|
||||
|
||||
allItemsPromises.push(p1);
|
||||
});
|
||||
|
||||
// Map refinement results
|
||||
refinementRows.map((refiner) => {
|
||||
|
||||
let values: IRefinementValue[] = [];
|
||||
refiner.Entries.map((item) => {
|
||||
values.push({
|
||||
RefinementCount: item.RefinementCount,
|
||||
RefinementName: this._formatDate(item.RefinementName), //This value will appear in the selected filter bar
|
||||
RefinementToken: item.RefinementToken,
|
||||
RefinementValue: this._formatDate(item.RefinementValue), // This value will appear in the filter panel
|
||||
});
|
||||
});
|
||||
|
||||
refinementResults.push({
|
||||
FilterName: refiner.Name,
|
||||
Values: values,
|
||||
});
|
||||
});
|
||||
|
||||
// Resolve all the promises once to get news
|
||||
const relevantResults: ISearchResult[] = await Promise.all(allItemsPromises);
|
||||
|
||||
// Sort refiners according to the property pane value
|
||||
refinementResults = sortBy(refinementResults, (refinement) => {
|
||||
|
||||
// Get the index of the corresponding filter name
|
||||
return sortedRefiners.indexOf(refinement.FilterName);
|
||||
});
|
||||
|
||||
results.RelevantResults = relevantResults;
|
||||
results.RefinementResults = refinementResults,
|
||||
results.TotalRows = r.TotalRows;
|
||||
}
|
||||
return results;
|
||||
|
||||
} catch (error) {
|
||||
Logger.write("[SharePointDataProvider.search()]: Error: " + error, LogLevel.Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the icon corresponding to the file name extension
|
||||
* @param filename The file name (ex: file.pdf)
|
||||
*/
|
||||
private async _mapToIcon(filename: string): Promise<string> {
|
||||
|
||||
const webAbsoluteUrl = this._context.pageContext.web.absoluteUrl;
|
||||
const web = new Web(webAbsoluteUrl);
|
||||
|
||||
try {
|
||||
const encodedFileName = filename ? filename.replace(/["']/g, "") : "";
|
||||
const iconFileName = await web.mapToIcon(encodedFileName,1);
|
||||
const iconUrl = webAbsoluteUrl + "/_layouts/15/images/" + iconFileName;
|
||||
|
||||
return iconUrl;
|
||||
|
||||
} catch (error) {
|
||||
Logger.write("[SharePointDataProvider._mapToIcon()]: Error: " + error, LogLevel.Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and eeplace ISO 8601 dates in the string by a friendly value
|
||||
* @param inputValue The string to format
|
||||
*/
|
||||
private _formatDate(inputValue: string): string {
|
||||
|
||||
const iso8061rgx = /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/g;
|
||||
const matches = inputValue.match(iso8061rgx);
|
||||
|
||||
let updatedInputValue = inputValue;
|
||||
|
||||
if (matches) {
|
||||
matches.map(match => {
|
||||
updatedInputValue = updatedInputValue.replace(match, moment(match).format("LL"));
|
||||
});
|
||||
}
|
||||
|
||||
return updatedInputValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the refinement condition in FQL format
|
||||
* @param selectedFilters The selected filter array
|
||||
*/
|
||||
private _buildRefinementQueryString(selectedFilters: IRefinementFilter[]): string {
|
||||
|
||||
let refinementQueryConditions: string[] = [];
|
||||
let refinementQueryString: string = null;
|
||||
|
||||
const refinementFilters = mapValues(groupBy(selectedFilters, 'FilterName'), (values) => {
|
||||
const refinementFilter = values.map((filter) => {
|
||||
return filter.Value.RefinementToken;
|
||||
});
|
||||
|
||||
return refinementFilter.length > 1 ? "or(" + refinementFilter + ")" : refinementFilter.toString();
|
||||
});
|
||||
|
||||
mapKeys(refinementFilters, (value, key) => {
|
||||
refinementQueryConditions.push(key + ":" + value);
|
||||
});
|
||||
|
||||
const conditionsCount = refinementQueryConditions.length;
|
||||
|
||||
switch (true) {
|
||||
|
||||
// No filters
|
||||
case (conditionsCount === 0): {
|
||||
refinementQueryString = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// Just one filter
|
||||
case (conditionsCount === 1): {
|
||||
refinementQueryString = refinementQueryConditions[0].toString();
|
||||
break;
|
||||
}
|
||||
|
||||
// Multiple filters
|
||||
case (conditionsCount > 1): {
|
||||
refinementQueryString = "and(" + refinementQueryConditions.toString() + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return refinementQueryString;
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchDataProvider;
|
|
@ -0,0 +1,27 @@
|
|||
export interface ISearchResults {
|
||||
RelevantResults: ISearchResult[];
|
||||
RefinementResults: IRefinementResult[];
|
||||
TotalRows?: number;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
[key: string]: string;
|
||||
IconSrc?: string;
|
||||
}
|
||||
|
||||
export interface IRefinementResult {
|
||||
FilterName: string;
|
||||
Values: IRefinementValue[];
|
||||
}
|
||||
|
||||
export interface IRefinementValue {
|
||||
RefinementCount: number;
|
||||
RefinementName: string;
|
||||
RefinementToken: string;
|
||||
RefinementValue: string;
|
||||
}
|
||||
|
||||
export interface IRefinementFilter {
|
||||
FilterName: string;
|
||||
Value: IRefinementValue;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { IRefinementFilter } from "./ISearchResult";
|
||||
|
||||
type RefinementFilterOperationCallback = (filters: IRefinementFilter[]) => void;
|
||||
|
||||
export default RefinementFilterOperationCallback;
|
|
@ -0,0 +1,7 @@
|
|||
export interface ISearchWebPartProps {
|
||||
searchQuery: string;
|
||||
maxResultsCount: number;
|
||||
selectedProperties: string;
|
||||
refiners: string;
|
||||
showPaging: boolean;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "42ad2740-3c60-49cf-971a-c44e33511b93",
|
||||
"alias": "SearchWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "PnP" },
|
||||
"title": { "default": "Search with Refiners" },
|
||||
"description": { "default": "Displays search results with customizable dynamic refiners" },
|
||||
"officeFabricIconFontName": "Search",
|
||||
"properties": {
|
||||
"searchQuery": "",
|
||||
"refiners": "",
|
||||
"selectedProperties": "",
|
||||
"maxResultsCount": 10
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
PropertyPaneSlider,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneToggle
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import * as strings from 'SearchWebPartStrings';
|
||||
import SearchContainer from "./components/SearchContainer/SearchContainer";
|
||||
import ISearchContainerProps from "./components/SearchContainer/ISearchContainerProps";
|
||||
import { ISearchWebPartProps } from './ISearchWebPartProps';
|
||||
import ISearchDataProvider from "../dataProviders/ISearchDataProvider";
|
||||
import MockSearchDataProvider from "../dataProviders/MockSearchDataProvider";
|
||||
import SearchDataProvider from "../dataProviders/SearchDataProvider";
|
||||
import * as moment from "moment";
|
||||
import { Placeholder, IPlaceholderProps } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
|
||||
export default class SearchWebPart extends BaseClientSideWebPart<ISearchWebPartProps> {
|
||||
|
||||
private _dataProvider: ISearchDataProvider;
|
||||
|
||||
/**
|
||||
* Override the base onInit() implementation to get the persisted properties to initialize data provider.
|
||||
*/
|
||||
protected onInit(): Promise<void> {
|
||||
|
||||
// Init the moment JS library locale globally
|
||||
const currentLocale = this.context.pageContext.cultureInfo.currentCultureName;
|
||||
moment.locale(currentLocale);
|
||||
|
||||
if (Environment.type === EnvironmentType.Local) {
|
||||
this._dataProvider = new MockSearchDataProvider();
|
||||
} else {
|
||||
this._dataProvider = new SearchDataProvider(this.context);
|
||||
}
|
||||
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
|
||||
this._dataProvider.resultsCount = this.properties.maxResultsCount;
|
||||
this._dataProvider.selectedProperties = this.properties.selectedProperties ?
|
||||
this.properties.selectedProperties.replace(/\s|,+$/g,'').split(",") :[];
|
||||
|
||||
let renderElement = null;
|
||||
|
||||
const searchContainer: React.ReactElement<ISearchContainerProps> = React.createElement(
|
||||
SearchContainer,
|
||||
{
|
||||
dataProvider: this._dataProvider,
|
||||
searchQuery: this.properties.searchQuery,
|
||||
maxResultsCount: this.properties.maxResultsCount,
|
||||
selectedProperties: this.properties.selectedProperties,
|
||||
refiners: this.properties.refiners,
|
||||
showPaging: this.properties.showPaging,
|
||||
}
|
||||
);
|
||||
|
||||
const placeholder: React.ReactElement<IPlaceholderProps> = React.createElement(
|
||||
Placeholder,
|
||||
{
|
||||
iconName: strings.PlaceHolderEditLabel,
|
||||
iconText: strings.PlaceHolderIconText,
|
||||
description: strings.PlaceHolderDescription,
|
||||
buttonLabel: strings.PlaceHolderConfigureBtnLabel,
|
||||
onConfigure: this._setupWebPart.bind(this)
|
||||
}
|
||||
);
|
||||
|
||||
renderElement = this.properties.searchQuery ? searchContainer : placeholder;
|
||||
|
||||
ReactDom.render(renderElement, this.domElement);
|
||||
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.SearchSettingsGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('searchQuery', {
|
||||
label: strings.SearchQueryFieldLabel,
|
||||
value: "Path:{Site}",
|
||||
multiline: true,
|
||||
resizable: true,
|
||||
placeholder: strings.SearchQueryPlaceHolderText,
|
||||
onGetErrorMessage: this._validateEmptyField.bind(this)
|
||||
}),
|
||||
PropertyPaneTextField('selectedProperties', {
|
||||
label: strings.SelectedPropertiesFieldLabel,
|
||||
multiline: true,
|
||||
resizable: true,
|
||||
value: "Title,Path,Created,Filename,ServerRedirectedPreviewURL",
|
||||
}),
|
||||
PropertyPaneTextField('refiners', {
|
||||
label: strings.RefinersFieldLabel,
|
||||
multiline: true,
|
||||
resizable: true,
|
||||
value: "Created"
|
||||
}),
|
||||
PropertyPaneSlider("maxResultsCount", {
|
||||
label: strings.MaxResultsCount,
|
||||
max: 50,
|
||||
min: 1,
|
||||
showValue: true,
|
||||
step: 1,
|
||||
value: 50,
|
||||
}),
|
||||
PropertyPaneToggle("showPaging", {
|
||||
label: strings.ShowPagingLabel,
|
||||
checked: false,
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Web Part property pane
|
||||
*/
|
||||
private _setupWebPart() {
|
||||
this.context.propertyPane.open();
|
||||
}
|
||||
|
||||
private _validateEmptyField(value: string): string {
|
||||
|
||||
if (!value) {
|
||||
return strings.EmptyFieldErrorMessage;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
import * as React from "react";
|
||||
import IFilterPanelProps from "./IFilterPanelProps";
|
||||
import IFilterPanelState from "./IFilterPanelState";
|
||||
import { PrimaryButton, DefaultButton, IButtonProps } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
||||
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||
import * as strings from "SearchWebPartStrings";
|
||||
import { IRefinementResult, IRefinementValue, IRefinementFilter } from "../../../models/ISearchResult";
|
||||
import { Link } from 'office-ui-fabric-react/lib/Link';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Text } from "@microsoft/sp-core-library";
|
||||
import "../SearchWebPart.scss";
|
||||
import * as update from "immutability-helper";
|
||||
import {
|
||||
GroupedList,
|
||||
IGroup,
|
||||
IGroupDividerProps
|
||||
} from 'office-ui-fabric-react/lib/components/GroupedList/index';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
|
||||
export default class FilterPanel extends React.Component<IFilterPanelProps, IFilterPanelState> {
|
||||
|
||||
private _initialFilters: IRefinementResult[];
|
||||
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
// The initialFilters are just set once and never updated afterwards so we don't need to put them in the component state.
|
||||
// We dont' want the refiners update every time to be able to revert changes easily in the interface and don't lose initial refiners.
|
||||
this._initialFilters = this.props.availableFilters;
|
||||
|
||||
this.state = {
|
||||
showPanel: false,
|
||||
selectedFilters: [],
|
||||
expandedGroups: [],
|
||||
};
|
||||
|
||||
this._onTogglePanel = this._onTogglePanel.bind(this);
|
||||
this._onClosePanel = this._onClosePanel.bind(this);
|
||||
this._addFilter = this._addFilter.bind(this);
|
||||
this._removeFilter = this._removeFilter.bind(this);
|
||||
this._isInFilterSelection = this._isInFilterSelection.bind(this);
|
||||
this._applyAllfilters = this._applyAllfilters.bind(this);
|
||||
this._removeAllFilters = this._removeAllFilters.bind(this);
|
||||
this._onRenderHeader = this._onRenderHeader.bind(this);
|
||||
this._onRenderCell = this._onRenderCell.bind(this);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IFilterPanelProps> {
|
||||
|
||||
let items: JSX.Element[] = [];
|
||||
let groups: IGroup[] = [];
|
||||
|
||||
// Initialize the Office UI grouped list
|
||||
this._initialFilters.map((filter, i) => {
|
||||
|
||||
groups.push({
|
||||
key: i.toString(),
|
||||
name: filter.FilterName,
|
||||
count: 1,
|
||||
startIndex: i,
|
||||
isDropEnabled: true,
|
||||
isCollapsed: this.state.expandedGroups.indexOf(i) === -1 ? true : false,
|
||||
});
|
||||
|
||||
items.push(
|
||||
<div key= { i }>
|
||||
<div className="filterPanel__filterProperty">
|
||||
{
|
||||
filter.Values.map((refinementValue: IRefinementValue, j) => {
|
||||
|
||||
// Create a new IRefinementFilter with only the current refinement information
|
||||
const currentRefinement: IRefinementFilter = {
|
||||
FilterName: filter.FilterName,
|
||||
Value: refinementValue,
|
||||
};
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
key={ j }
|
||||
checked= { this._isInFilterSelection(currentRefinement) }
|
||||
disabled={ false }
|
||||
label={ Text.format(refinementValue.RefinementValue + " ({0})", refinementValue.RefinementCount)}
|
||||
onChanged= {(checked: boolean) => {
|
||||
// Every time we chek/uncheck a filter, a complete new search request is performed with current selected refiners
|
||||
checked ? this._addFilter(currentRefinement): this._removeFilter(currentRefinement);
|
||||
}} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const renderSelectedFilters: JSX.Element[] = this.state.selectedFilters.map((filter) => {
|
||||
|
||||
return (
|
||||
<PrimaryButton
|
||||
key ={filter.Value.RefinementToken }
|
||||
className="searchWp__selectedFilters__filterBtn"
|
||||
iconProps={ { iconName: 'StatusErrorFull' } }
|
||||
text={ filter.Value.RefinementName }
|
||||
onClick={ ()=> { this._removeFilter(filter); }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const renderAvailableFilters = <GroupedList
|
||||
ref='groupedList'
|
||||
items={ items }
|
||||
onRenderCell={ this._onRenderCell }
|
||||
className="filterPanel__body__group"
|
||||
groupProps={
|
||||
{
|
||||
onRenderHeader: this._onRenderHeader,
|
||||
}
|
||||
}
|
||||
groups={ groups }/>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DefaultButton
|
||||
className="searchWp__filterResultBtn"
|
||||
iconProps={ { iconName: 'Filter' } }
|
||||
text={ strings.FilterResultsButtonLabel }
|
||||
onClick= { this._onTogglePanel }
|
||||
/>
|
||||
{ (this.state.selectedFilters.length > 0) ?
|
||||
|
||||
<div className="searchWp__selectedFilters">
|
||||
<Label className="searchWp__selectedFilterLbl ms-font-s">{ strings.SelectedFiltersLabel }</Label>
|
||||
{ renderSelectedFilters }
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<Panel
|
||||
className="filterPanel"
|
||||
isOpen={ this.state.showPanel }
|
||||
type={ PanelType.smallFixedFar }
|
||||
isBlocking={ false }
|
||||
isLightDismiss= { true }
|
||||
onDismiss={ this._onClosePanel }
|
||||
headerText={ strings.FilterPanelTitle }
|
||||
closeButtonAriaLabel='Close'
|
||||
hasCloseButton={ true }
|
||||
headerClassName="filterPanel__header"
|
||||
onRenderBody={() => {
|
||||
if(this._initialFilters.length > 0) {
|
||||
return (
|
||||
<Scrollbars style={{ height: "100%" }}>
|
||||
<div className="filterPanel__body">
|
||||
<div className="filterPanel__body__allFiltersToggle">
|
||||
<Toggle
|
||||
onText={ strings.RemoveAllFiltersLabel }
|
||||
offText={ strings.ApplyAllFiltersLabel }
|
||||
onChanged= {(checked: boolean) => {
|
||||
checked ? this._applyAllfilters() : this._removeAllFilters();
|
||||
}}
|
||||
checked= { this.state.selectedFilters.length === 0 ? false : true }
|
||||
/>
|
||||
</div>
|
||||
{ renderAvailableFilters }
|
||||
</div>
|
||||
</Scrollbars>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="filterPanel__body">
|
||||
{ strings.NoFilterConfiguredLabel }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}}>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _onRenderCell(nestingDepth: number, item: any, itemIndex: number) {
|
||||
return (
|
||||
<div className="ms-Grid-row" data-selection-index={ itemIndex }>
|
||||
<div className="ms-Grid-col ms-u-sm10 ms-u-md10 ms-u-lg10 ms-smPush1 ms-mdPush1 ms-lgPush1">
|
||||
{ item }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _onRenderHeader(props: IGroupDividerProps): JSX.Element {
|
||||
return (
|
||||
|
||||
<div className="ms-Grid-row" onClick={ () => {
|
||||
|
||||
// Update the index for expanded groups to be able to keep it open after a re-render
|
||||
const updatedExpandedGroups =
|
||||
props.group.isCollapsed ?
|
||||
update(this.state.expandedGroups, {$push: [props.group.startIndex]}) :
|
||||
update(this.state.expandedGroups, {$splice: [[this.state.expandedGroups.indexOf(props.group.startIndex), 1]]});
|
||||
|
||||
this.setState({
|
||||
expandedGroups: updatedExpandedGroups,
|
||||
});
|
||||
|
||||
props.onToggleCollapse(props.group);
|
||||
}}>
|
||||
<div className="ms-Grid-col ms-u-sm1 ms-u-md1 ms-u-lg1">
|
||||
<div className="header-icon">
|
||||
<i className={ props.group.isCollapsed ? "ms-Icon ms-Icon--ChevronDown" : "ms-Icon ms-Icon--ChevronUp"}></i>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ms-Grid-col ms-u-sm10 ms-u-md10 ms-u-lg10">
|
||||
<div className="ms-font-l">{ props.group.name }</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _onClosePanel() {
|
||||
this.setState({ showPanel: false });
|
||||
}
|
||||
|
||||
private _onTogglePanel() {
|
||||
this.setState({ showPanel: !this.state.showPanel });
|
||||
}
|
||||
|
||||
private _addFilter(filterToAdd: IRefinementFilter): void {
|
||||
|
||||
// Add the filter to the selected filters collection
|
||||
let newFilters = update(this.state.selectedFilters, {$push: [filterToAdd]});
|
||||
this._applyFilters(newFilters);
|
||||
}
|
||||
|
||||
private _removeFilter(filterToRemove: IRefinementFilter): void {
|
||||
|
||||
// Remove the filter from the selected filters collection
|
||||
let newFilters = this.state.selectedFilters.filter((elt) => {
|
||||
return elt.Value.RefinementToken !== filterToRemove.Value.RefinementToken;
|
||||
});
|
||||
|
||||
this._applyFilters(newFilters);
|
||||
}
|
||||
|
||||
private _applyAllfilters(): void {
|
||||
|
||||
let allFilters: IRefinementFilter[] = [];
|
||||
|
||||
this._initialFilters.map((filter) => {
|
||||
|
||||
filter.Values.map((refinementValue: IRefinementValue, index) => {
|
||||
allFilters.push({FilterName: filter.FilterName, Value: refinementValue});
|
||||
});
|
||||
});
|
||||
|
||||
this._applyFilters(allFilters);
|
||||
}
|
||||
|
||||
private _removeAllFilters(): void {
|
||||
this._applyFilters([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner method to effectivly apply the refiners by calling back the parent component
|
||||
* @param selectedFilters The filters to apply
|
||||
*/
|
||||
private _applyFilters(selectedFilters: IRefinementFilter[]): void {
|
||||
|
||||
// Save the selected filters
|
||||
this.setState({
|
||||
selectedFilters: selectedFilters,
|
||||
});
|
||||
|
||||
this.props.onUpdateFilters(selectedFilters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current filter is present in the list of the selected filters
|
||||
* @param filterToCheck The filter to check
|
||||
*/
|
||||
private _isInFilterSelection(filterToCheck: IRefinementFilter): boolean {
|
||||
|
||||
let newFilters = this.state.selectedFilters.filter((filter) => {
|
||||
return filter.Value.RefinementToken === filterToCheck.Value.RefinementToken;
|
||||
});
|
||||
|
||||
return newFilters.length === 0 ? false : true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { IRefinementResult } from "../../../models/ISearchResult";
|
||||
import RefinementFilterOperationCallback from "../../../models/RefinementValueOperationCallback";
|
||||
|
||||
interface IFilterPanelProps {
|
||||
availableFilters: IRefinementResult[];
|
||||
onUpdateFilters: RefinementFilterOperationCallback;
|
||||
}
|
||||
|
||||
export default IFilterPanelProps;
|
|
@ -0,0 +1,9 @@
|
|||
import { IRefinementFilter } from "../../../models/ISearchResult";
|
||||
|
||||
interface IFilterPanelState {
|
||||
showPanel?: boolean;
|
||||
selectedFilters?: IRefinementFilter[];
|
||||
expandedGroups?: number[];
|
||||
}
|
||||
|
||||
export default IFilterPanelState;
|
|
@ -0,0 +1,10 @@
|
|||
export type PageUpdateCallback = (pageNumber: number) => void;
|
||||
|
||||
interface IPagingProps {
|
||||
totalItems: number;
|
||||
itemsCountPerPage: number;
|
||||
onPageUpdate: PageUpdateCallback;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
export default IPagingProps;
|
|
@ -0,0 +1,40 @@
|
|||
import * as React from "react";
|
||||
import IPagingProps from "./IPagingProps";
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import Pagination from "react-js-pagination";
|
||||
|
||||
export default class Paging extends React.Component<IPagingProps, null> {
|
||||
|
||||
constructor(props: IPagingProps) {
|
||||
super(props);
|
||||
|
||||
this._onPageUpdate = this._onPageUpdate.bind(this);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IPagingProps> {
|
||||
|
||||
return(
|
||||
<div className="searchWp__paginationContainer">
|
||||
<div className="searchWp__paginationContainer__pagination">
|
||||
<Pagination
|
||||
activePage={this.props.currentPage}
|
||||
firstPageText={<i className="ms-Icon ms-Icon--DoubleChevronLeft" aria-hidden="true"></i>}
|
||||
lastPageText={<i className="ms-Icon ms-Icon--DoubleChevronRight" aria-hidden="true"></i>}
|
||||
prevPageText={<i className="ms-Icon ms-Icon--ChevronLeft" aria-hidden="true"></i>}
|
||||
nextPageText={<i className="ms-Icon ms-Icon--ChevronRight" aria-hidden="true"></i>}
|
||||
activeLinkClass={ "active" }
|
||||
itemsCountPerPage={ this.props.itemsCountPerPage }
|
||||
totalItemsCount={ this.props.totalItems }
|
||||
pageRangeDisplayed={5}
|
||||
onChange={this.props.onPageUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _onPageUpdate(pageNumber: number): void {
|
||||
this.props.onPageUpdate(pageNumber);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import ISearchDataProvider from "../../../dataProviders/ISearchDataProvider";
|
||||
|
||||
interface ISearchContainerProps {
|
||||
dataProvider: ISearchDataProvider;
|
||||
searchQuery: string;
|
||||
maxResultsCount: number;
|
||||
selectedProperties: string[];
|
||||
refiners: string;
|
||||
showPaging: boolean;
|
||||
}
|
||||
|
||||
export default ISearchContainerProps;
|
|
@ -0,0 +1,12 @@
|
|||
import { ISearchResults, IRefinementFilter } from "../../../models/ISearchResult";
|
||||
|
||||
interface ISearchContainerState {
|
||||
results?: ISearchResults;
|
||||
selectedFilters?: IRefinementFilter[];
|
||||
currentPage?: number;
|
||||
errorMessage?: string;
|
||||
hasError?: boolean;
|
||||
areResultsLoading?: boolean;
|
||||
}
|
||||
|
||||
export default ISearchContainerState;
|
|
@ -0,0 +1,154 @@
|
|||
import * as React from "react";
|
||||
import ISearchContainerProps from "./ISearchContainerProps";
|
||||
import ISearchContainerState from "./ISearchContainerState";
|
||||
import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar";
|
||||
import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner";
|
||||
import { Logger, LogLevel } from "sp-pnp-js";
|
||||
import * as strings from "SearchWebPartStrings";
|
||||
import { ISearchResults, IRefinementFilter } from "../../../models/ISearchResult";
|
||||
import TilesList from "../TilesList/TilesList";
|
||||
import "../SearchWebPart.scss";
|
||||
import FilterPanel from "../FilterPanel/FilterPanel";
|
||||
import Paging from "../Paging/Paging";
|
||||
|
||||
export default class SearchContainer extends React.Component<ISearchContainerProps,ISearchContainerState> {
|
||||
|
||||
public constructor(props) {
|
||||
|
||||
super(props);
|
||||
|
||||
// Set the initial state
|
||||
this.state = {
|
||||
results: {
|
||||
RefinementResults: [],
|
||||
RelevantResults: []
|
||||
},
|
||||
selectedFilters: [],
|
||||
currentPage: 1,
|
||||
areResultsLoading: true,
|
||||
errorMessage: "",
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
this._onUpdateFilters = this._onUpdateFilters.bind(this);
|
||||
this._onPageUpdate = this._onPageUpdate.bind(this);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<ISearchContainerProps> {
|
||||
|
||||
const areResultsLoading = this.state.areResultsLoading;
|
||||
const items = this.state.results;
|
||||
const hasError = this.state.hasError;
|
||||
const errorMessage = this.state.errorMessage;
|
||||
|
||||
let wpContent: JSX.Element = null;
|
||||
|
||||
if (areResultsLoading) {
|
||||
wpContent = <Spinner size={ SpinnerSize.large } label={ strings.LoadingMessage } />;
|
||||
} else {
|
||||
|
||||
if (hasError) {
|
||||
|
||||
wpContent = <MessageBar messageBarType= { MessageBarType.error }>{ errorMessage }</MessageBar>;
|
||||
|
||||
} else {
|
||||
|
||||
if (items.RelevantResults.length === 0) {
|
||||
|
||||
wpContent =
|
||||
<div>
|
||||
<FilterPanel availableFilters={ items.RefinementResults } onUpdateFilters={ this._onUpdateFilters }/>
|
||||
<div className="searchWp__noresult">{ strings.NoResultMessage }</div>
|
||||
</div>;
|
||||
|
||||
} else {
|
||||
|
||||
wpContent =
|
||||
|
||||
<div>
|
||||
<FilterPanel availableFilters={ items.RefinementResults } onUpdateFilters={ this._onUpdateFilters }/>
|
||||
<TilesList items={ items.RelevantResults }/>
|
||||
{ this.props.showPaging ?
|
||||
<Paging
|
||||
totalItems={ items.TotalRows }
|
||||
itemsCountPerPage={ this.props.maxResultsCount }
|
||||
onPageUpdate={ this._onPageUpdate }
|
||||
currentPage={ this.state.currentPage }/>
|
||||
: null
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="searchWp">
|
||||
{ wpContent }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
||||
// Async calls
|
||||
this._getSearchResults(this.props.searchQuery, this.props.refiners, this.state.selectedFilters, this.state.currentPage);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: ISearchContainerProps): void {
|
||||
|
||||
// Intermediate state to display the spinner before an async query
|
||||
this.setState({
|
||||
areResultsLoading: true,
|
||||
});
|
||||
|
||||
// We reset the page number and refinement filters
|
||||
this._getSearchResults(nextProps.searchQuery, nextProps.refiners, [], 1);
|
||||
}
|
||||
|
||||
private _getSearchResults(searchQuery: string, refiners: string, refinementFilters?: IRefinementFilter[], pageNumber?: number) {
|
||||
|
||||
this.props.dataProvider.search(searchQuery, refiners, refinementFilters, pageNumber).then((searchResults: ISearchResults) => {
|
||||
|
||||
this.setState({
|
||||
results: searchResults,
|
||||
areResultsLoading: false,
|
||||
});
|
||||
|
||||
}).catch((error) => {
|
||||
Logger.write("[SearchContainer._getSearchResults()]: Error: " + error, LogLevel.Error);
|
||||
|
||||
this.setState({
|
||||
areResultsLoading: false,
|
||||
results: { RefinementResults: [], RelevantResults: [] },
|
||||
hasError: true,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function to apply new filters coming from the filter panel child component
|
||||
* @param newFilters The new filters to apply
|
||||
*/
|
||||
private _onUpdateFilters(newFilters: IRefinementFilter[]) {
|
||||
|
||||
this._getSearchResults(this.props.searchQuery, this.props.refiners, newFilters, 1);
|
||||
|
||||
this.setState({
|
||||
selectedFilters: newFilters,
|
||||
currentPage: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function update search results according the page number
|
||||
* @param pageNumber The page mumber to get
|
||||
*/
|
||||
private _onPageUpdate(pageNumber: number) {
|
||||
this._getSearchResults(this.props.searchQuery, this.props.refiners, this.state.selectedFilters, pageNumber);
|
||||
|
||||
this.setState({
|
||||
currentPage: pageNumber,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
.searchWp {
|
||||
|
||||
&__tile {
|
||||
float: left;
|
||||
|
||||
&__footer {
|
||||
padding: 8px 16px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&__footerInfoRight {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 8px 16px;
|
||||
color: #ffffff;
|
||||
background-color: #0078d7;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__iconContainer {
|
||||
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__resultCard {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__noresult {
|
||||
padding:10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__filterResultBtn, &__selectedFilterLbl {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
&__selectedFilters {
|
||||
|
||||
&__filterBtn {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__paginationContainer {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
|
||||
.searchWp__paginationContainer__pagination {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
ul {
|
||||
display: inline-block;
|
||||
padding-left: 0;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
|
||||
a {
|
||||
float: left;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 15px;
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a.active {
|
||||
background-color: #0078d7;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filterPanel {
|
||||
|
||||
&__body {
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
|
||||
.header-icon {
|
||||
text-align: right;
|
||||
margin-bottom:8px;
|
||||
|
||||
.ms-Icon {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&.ms-Icon--CalculatorSubtract, &.ms-Icon--CalculatorAddition {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__allFiltersToggle {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
&__group {
|
||||
|
||||
.ms-List-page~.ms-List-page {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ISearchResult } from "../../../models/ISearchResult";
|
||||
|
||||
interface ITileProps {
|
||||
item: ISearchResult;
|
||||
}
|
||||
|
||||
export default ITileProps;
|
|
@ -0,0 +1,7 @@
|
|||
import { ISearchResult } from "../../../models/ISearchResult";
|
||||
|
||||
interface ITilesListViewProps {
|
||||
items?: ISearchResult[];
|
||||
}
|
||||
|
||||
export default ITilesListViewProps;
|
|
@ -0,0 +1,49 @@
|
|||
import * as React from "react";
|
||||
import ITileProps from "./ITileProps";
|
||||
import {
|
||||
DocumentCard,
|
||||
DocumentCardActions,
|
||||
DocumentCardActivity,
|
||||
DocumentCardLocation,
|
||||
DocumentCardPreview,
|
||||
DocumentCardTitle,
|
||||
IDocumentCardPreviewProps
|
||||
} from 'office-ui-fabric-react/lib/DocumentCard';
|
||||
import { ImageFit } from 'office-ui-fabric-react/lib/Image';
|
||||
import * as moment from "moment";
|
||||
import "../SearchWebPart.scss";
|
||||
|
||||
const PREVIEW_IMAGE_WIDTH: number = 204;
|
||||
const PREVIEW_IMAGE_HEIGHT: number = 111;
|
||||
|
||||
export default class Tile extends React.Component<ITileProps, null> {
|
||||
|
||||
public render() {
|
||||
|
||||
const item = this.props.item;
|
||||
|
||||
let previewProps: IDocumentCardPreviewProps = {
|
||||
previewImages: [
|
||||
{
|
||||
url: item.ServerRedirectedURL ? item.ServerRedirectedURL : item.Path,
|
||||
previewImageSrc: item.ServerRedirectedPreviewURL,
|
||||
iconSrc: item.iconSrc,
|
||||
imageFit: ImageFit.cover,
|
||||
height: PREVIEW_IMAGE_HEIGHT,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<DocumentCard onClickHref={ item.ServerRedirectedURL ? item.ServerRedirectedURL : item.Path } className="searchWp__resultCard">
|
||||
<div className="searchWp__tile__iconContainer" style={{ "height": PREVIEW_IMAGE_HEIGHT }}>
|
||||
<DocumentCardPreview { ...previewProps } />
|
||||
</div>
|
||||
<DocumentCardTitle title={ item.Title } shouldTruncate={ false } />
|
||||
<div className="searchWp__tile__footer">
|
||||
<span>{ moment(item.Created).isValid() ? moment(item.Created).format("L"): null }</span>
|
||||
</div>
|
||||
</DocumentCard>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import * as React from "react";
|
||||
import ITilesListViewProps from "./ITilesListViewProps";
|
||||
import { List } from 'office-ui-fabric-react/lib/List';
|
||||
import Tile from "./Tile";
|
||||
import { IRectangle } from "office-ui-fabric-react/lib/Utilities";
|
||||
import "../SearchWebPart.scss";
|
||||
|
||||
const ROWS_PER_PAGE = 3;
|
||||
const MAX_ROW_HEIGHT = 300;
|
||||
|
||||
export default class TilesList extends React.Component<ITilesListViewProps, null> {
|
||||
|
||||
private _positions: any;
|
||||
private _columnCount: number;
|
||||
private _columnWidth: number;
|
||||
private _rowHeight: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._positions = {};
|
||||
this._getItemCountForPage = this._getItemCountForPage.bind(this);
|
||||
this._getPageHeight = this._getPageHeight.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
||||
const items = this.props.items;
|
||||
|
||||
return (
|
||||
<List
|
||||
items={ items }
|
||||
getItemCountForPage={ this._getItemCountForPage }
|
||||
getPageHeight={ this._getPageHeight }
|
||||
renderedWindowsAhead={ 4 }
|
||||
className="searchWp__list"
|
||||
onRenderCell={ (item, index) => (
|
||||
<div className="searchWp__tile"
|
||||
style={ {
|
||||
width: (100 / this._columnCount) + '%',
|
||||
} }>
|
||||
<Tile key={ index } item= { item }/>
|
||||
</div>
|
||||
)}/>
|
||||
);
|
||||
}
|
||||
|
||||
private _getItemCountForPage(itemIndex: number, surfaceRect: IRectangle) {
|
||||
if (itemIndex === 0) {
|
||||
this._columnCount = Math.ceil(surfaceRect.width / MAX_ROW_HEIGHT);
|
||||
this._columnWidth = Math.floor(surfaceRect.width / this._columnCount);
|
||||
this._rowHeight = this._columnWidth;
|
||||
}
|
||||
|
||||
return this._columnCount * ROWS_PER_PAGE;
|
||||
}
|
||||
|
||||
private _getPageHeight(itemIndex: number, surfaceRect: IRectangle) {
|
||||
return this._rowHeight * ROWS_PER_PAGE;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"SearchSettingsGroupName": "Search settings",
|
||||
"SearchQueryFieldLabel": "Search query",
|
||||
"SelectedPropertiesFieldLabel": "Selected Properties",
|
||||
"LoadingMessage": "Results are loading, please wait...",
|
||||
"MaxResultsCount": "Number of items to retrieve per page",
|
||||
"NoResultMessage": "There is no items to show",
|
||||
"RefinersFieldLabel": "Refiners",
|
||||
"FilterPanelTitle": "Available filters",
|
||||
"FilterResultsButtonLabel": "Filter results",
|
||||
"SelectedFiltersLabel": "Selected filters:",
|
||||
"ApplyAllFiltersLabel": "Apply all filters",
|
||||
"RemoveAllFiltersLabel": "Remove all filters",
|
||||
"ShowPagingLabel": "Show paging",
|
||||
"NoFilterConfiguredLabel": "No filter configured",
|
||||
"SearchQueryPlaceHolderText": "Search query in KQL format",
|
||||
"EmptyFieldErrorMessage": "This field cannot be empty",
|
||||
"PlaceHolderEditLabel": "Edit",
|
||||
"PlaceHolderConfigureBtnLabel": "Configure",
|
||||
"PlaceHolderIconText": "Search Results Web Part with Refinements",
|
||||
"PlaceHolderDescription": "This component displays search results with paging and customizable refinement panel"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"SearchSettingsGroupName": "Paramètres de recherche",
|
||||
"SearchQueryFieldLabel": "Requête de recherche",
|
||||
"SelectedPropertiesFieldLabel": "Propriétés à récupérer",
|
||||
"LoadingMessage": "Les résultats sont en cours de chargement, veuillez patienter...",
|
||||
"MaxResultsCount": "Nombre de résulats à récupérer par page",
|
||||
"NoResultMessage": "Il n'y a aucun élément à afficher.",
|
||||
"RefinersFieldLabel": "Filtres",
|
||||
"FilterPanelTitle": "Filtres disponibles",
|
||||
"FilterResultsButtonLabel": "Filtrer l'affichage",
|
||||
"SelectedFiltersLabel": "Filtre(s) appliqué(s):",
|
||||
"ApplyAllFiltersLabel": "Appliquer tous les filters",
|
||||
"RemoveAllFiltersLabel": "Supprimer tous les filtres",
|
||||
"ShowPagingLabel": "Afficher la pagination",
|
||||
"NoFilterConfiguredLabel": "Aucun filtre configuré",
|
||||
"SearchQueryPlaceHolderText": "Requête de recherche au format KQL",
|
||||
"EmptyFieldErrorMessage": "Ce champ ne peut pas être vide",
|
||||
"PlaceHolderEditLabel": "Éditer",
|
||||
"PlaceHolderConfigureBtnLabel": "Configurer",
|
||||
"PlaceHolderIconText": "Web Part de recherche avec affinements",
|
||||
"PlaceHolderDescription": "Ce composant affiche une liste de résulats de recherche avec pagination et des filtres configurables"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
declare interface ISearchWebPartStrings {
|
||||
SearchSettingsGroupName: string;
|
||||
SearchQueryFieldLabel: string;
|
||||
SelectedPropertiesFieldLabel: string;
|
||||
LoadingMessage: string;
|
||||
MaxResultsCount: string;
|
||||
NoResultMessage: string;
|
||||
RefinersFieldLabel: string;
|
||||
FilterPanelTitle: string;
|
||||
FilterResultsButtonLabel: string;
|
||||
SelectedFiltersLabel: string;
|
||||
ApplyAllFiltersLabel: string;
|
||||
RemoveAllFiltersLabel: string;
|
||||
ShowPagingLabel: string;
|
||||
NoFilterConfiguredLabel: string;
|
||||
SearchQueryPlaceHolderText: string;
|
||||
EmptyFieldErrorMessage: string;
|
||||
PlaceHolderEditLabel: string;
|
||||
PlaceHolderConfigureBtnLabel: string;
|
||||
PlaceHolderIconText: string;
|
||||
PlaceHolderDescription: string;
|
||||
}
|
||||
|
||||
declare module 'SearchWebPartStrings' {
|
||||
const strings: ISearchWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="mocha" />
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
describe('SearchWebPart', () => {
|
||||
it('should do something', () => {
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"es6-collections",
|
||||
"webpack-env"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Type definitions for Microsoft ODSP projects
|
||||
// Project: ODSP
|
||||
|
||||
/* Global definition for UNIT_TEST builds
|
||||
Code that is wrapped inside an if(UNIT_TEST) {...}
|
||||
block will not be included in the final bundle when the
|
||||
--ship flag is specified */
|
||||
declare const UNIT_TEST: boolean;
|
||||
|
||||
/* Global defintion for SPO builds */
|
||||
declare const DATACENTER: boolean;
|
|
@ -0,0 +1 @@
|
|||
/// <reference path="@ms/odsp.d.ts" />
|
Loading…
Reference in New Issue