React Content Query WebPart (#271)
* Added the files to the new repository * Modified the .gitignore * Added the generated sppkg file * Added animated gifs for the read me. * Added another animated gif * Added a first temporary version of the readme.md * Update README.md * Update README.md * Updated the readme.md file by removing spaces between brackets in the handlebars templates examples. * Updated the WebPart with inline template editing Added inline template editing within the toolpane, automatic template generation and fixed a bug related to the "Me" checkbox while filtering user fields. * Removed the debug folder from sources * Updated the toolpart animated gif * Removed toolpart animated gif * Updated the README.md file with the latest changes. * Update README.md * Update README.md * Update README.md * Adding AceEditor as Code Editor * Removing old Monato Artefacts * Finalized the code editor update Added documentation for the code editor, fixed the npm-shrinkwrap to support the code editor dependencies, updated images for the new documentation, regenerated the .sppkg file and updated the sources on the public CDN for version 1.0.1 * Added a .gif for the new code editor documentation * Updated the WebPart with the latest spfx packages. Multiple bottle-neck bugs have been fixed in SPFX which allows to update the WebPart with the latest SPFX packages. Overall performance should now be better and other minor bugs have been fixed. * ... * Updated the readme
This commit is contained in:
parent
b8405c728a
commit
d821b0e8c7
|
@ -1,15 +1,15 @@
|
|||
import { IError } from "../../interfaces/IError";
|
||||
import * as angular from 'angular';
|
||||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { ServiceKey, ServiceScope } from '@microsoft/sp-core-library';
|
||||
|
||||
|
||||
export class BaseService {
|
||||
public static $inject: string[] = ["$http", "$q", "pageContext"];
|
||||
public static $inject: string[] = ["$http", "$q"];
|
||||
public baseUrl: string;
|
||||
|
||||
constructor(private $http: ng.IHttpService, private $q: ng.IQService, private pageContext:any) {
|
||||
this.baseUrl = pageContext.web.absoluteUrl;
|
||||
constructor(private $http: ng.IHttpService, private $q: ng.IQService) {
|
||||
|
||||
this.baseUrl = (<any>window)._spPageContextInfo.webAbsoluteUrl;
|
||||
|
||||
}
|
||||
|
||||
public getRequest(query?: string, endPoint?: string): ng.IPromise<any> {
|
||||
|
|
|
@ -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 @@
|
|||
* text=auto
|
|
@ -0,0 +1,31 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
temp
|
||||
solution/debug
|
||||
|
||||
# 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,14 @@
|
|||
# Folders
|
||||
.vscode
|
||||
coverage
|
||||
node_modules
|
||||
sharepoint
|
||||
src
|
||||
temp
|
||||
|
||||
# Files
|
||||
*.csproj
|
||||
.git*
|
||||
.yo-rc.json
|
||||
gulpfile.js
|
||||
tsconfig.json
|
|
@ -0,0 +1,75 @@
|
|||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
// Configure glob patterns for excluding files and folders in the file explorer.
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/bower_components": true,
|
||||
"**/coverage": true,
|
||||
"**/lib-amd": true,
|
||||
"src/**/*.scss.ts": true
|
||||
},
|
||||
"typescript.tsdk": ".\\node_modules\\typescript\\lib",
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/config.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-web/lib/schemas/config.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/copy-assets.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyAssets/copy-assets.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/deploy-azure-storage.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/deployAzureStorage/deploy-azure-storage.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/package-solution.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/packageSolution/package-solution.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/serve.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/gulp-core-build-serve/lib/serve.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/tslint.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/gulp-core-build-typescript/lib/schemas/tslint.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/write-manifests.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/writeManifests/write-manifests.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/configure-webpack.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/configure-external-bundling-webpack.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack-external-bundling.schema.json"
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
"/copy-static-assets.json"
|
||||
],
|
||||
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyStaticAssets/copy-static-assets.schema.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.1.1",
|
||||
"libraryName": "react-content-query",
|
||||
"libraryId": "489c9f8f-8e66-4efb-8365-85279ba91433",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 242 KiB |
Binary file not shown.
After Width: | Height: | Size: 716 KiB |
Binary file not shown.
After Width: | Height: | Size: 715 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.6 MiB |
Binary file not shown.
After Width: | Height: | Size: 9.7 MiB |
|
@ -0,0 +1,193 @@
|
|||
# React Content Query WebPart
|
||||
|
||||
## Summary
|
||||
|
||||
The `React Content Query WebPart` is a modern version of the good old `Content by Query WebPart` that was introduced in SharePoint 2007. Built for *SharePoint 2016* and *Office 365*, this modern version is built against the new **SharePoint Framework (SPFx)** and uses the latest *Web Stack* practices. While the original WebPart was based on a `XSLT` templating engine, this *React* WebPart is based on the well known [Handlebars templating engine](http://handlebarsjs.com), which empowers users to create simple, yet powerfull `HTML` templates for rendering the queried content. This new version also lets the user query `any site collections` which resides on the same domain url, add `unlimited filters`, query *DateTime* fields to the `nearest minute` rather than being limited to a day, and much more.
|
||||
|
||||
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/toolpart.gif" />
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-GA-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework Developer](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
|
||||
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0|May 04, 2017|Initial release
|
||||
1.0.1|July 23rd 15, 2017|Updated to GA Version
|
||||
|
||||
## 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.**
|
||||
|
||||
## Features
|
||||
|
||||
### Cross site collection
|
||||
|
||||
The WebPart uses the search in order to get all sites under the current domain, which makes it possible to query not only subsites but other site collections and their subsites as well.
|
||||
|
||||
![DateTime](https://github.com/spplante/react-content-query/blob/master/Misc/allsites.gif "DateTime")
|
||||
<br>
|
||||
|
||||
### Unlimited filters
|
||||
|
||||
The user isn't limited to 3 filters anymore, an unlimited amount of filters can be added to narrow down your query
|
||||
|
||||
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/filters.gif" width="500" />
|
||||
<br>
|
||||
|
||||
### Improved date time filters
|
||||
|
||||
It is now possible to include time validation when querying date fields, giving the ability to be more precise when it comes to querying items against date values.
|
||||
|
||||
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/datetime.gif" width="500" />
|
||||
<br>
|
||||
|
||||
### Handlebars templating engine
|
||||
|
||||
Enjoy a simple, yet powerfull html-based templating engine for rendering your results. The WebPart even generates a default Handlebars template for you based on the view fields you have selected during the configuration!
|
||||
|
||||
For advanced users, more than 150 Handlebars block helpers are available by default within the user defined template. For a list of all block helpers, see [handlebars-helpers](https://github.com/helpers/handlebars-helpers#helpers)
|
||||
<br>
|
||||
|
||||
### Built-in template editor
|
||||
|
||||
Edit your Handlebars template directly within the toolpane using a built-in [code editor](https://ace.c9.io/) which provides code folding, syntax highlighting, line wrapping, indentation and many more features to the tip of your fingers.
|
||||
|
||||
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/editor.gif" />
|
||||
<br>
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Adding the WebPart to your page
|
||||
|
||||
To add the `React Content Query WebPart` to your site page you have two options :
|
||||
- Either clone this repository, build the project yourself and connect it to SharePoint (see [officedev documentation](https://dev.office.com/sharepoint/docs/spfx/web-parts/get-started/connect-to-sharepoint))
|
||||
- Or download the `react-content-query-webpart.sppkg` file available in the `sharepoint/solution` folder of the repository and add it directly in your app catalog in order to be able to use it in your site.
|
||||
|
||||
Note : The second method will only work for Office 365 sites, since the **.ppkg** file points to an Office 365 public CDN url which expects the referer to come from a valid https://**\*.sharepoint.com\*** url.
|
||||
|
||||
### Configuring the WebPart
|
||||
|
||||
As seen in the [User friendly configuration](#user-friendly-configuration) section, configuring the WebPart is quite straight forward. However, here's a list of *gotchas* that could save you some time :
|
||||
|
||||
- The `Web Url` property uses the search to find all sites that are under the current domain. That being said, newly created sites can take a while to appear within the dropdown options, based on the search crawl schedule.
|
||||
- The `Filters` property still supports query string expressions like *[PageQueryString:ParamName]* for text fields, and date expressions such as *[Today]* or *[Today] + 4* for date fields.
|
||||
- The `Template` property stops getting automtically generated while selecting view fields as soon as the template is manually updated, in order to prevent unwanted loss of templating efforts.
|
||||
- The `Template Url` property has priority over the `Template` property, which means if a valid handlebars template url is provided, the inline template will be kept, but ignored at runtime.
|
||||
|
||||
### Designing your Handlebars template
|
||||
|
||||
#### Basics
|
||||
|
||||
Before anything, make sure you understand the basics of Handlebars and its associated syntax by reading their [documentation](http://handlebarsjs.com)
|
||||
|
||||
#### Available tokens
|
||||
|
||||
To make it simple, a `template context` is automatically exposed within the handlebars template, giving the user the ability to work with the following exposed tokens :
|
||||
|
||||
Property | Description
|
||||
-----------------|------------------
|
||||
{{items}} | The array of objects that represents the items returned from the CAML query
|
||||
{{pageContext}} | The SPFx [PageContext](https://github.com/SharePoint/sp-dev-docs/blob/master/reference/spfx/sp-page-context/pagecontext.md) object which contains usefull informations about the current web, list, user, language etc...
|
||||
{{accessDenied}} | A boolean value indicating if the current user has a denied access to the configured site that gets queried. This gives the designer the power to decide what to render in a case where the current user doesn't have access to the queried site.
|
||||
{{webNotFound}} | A boolean value indicating if the configured site that gets queried doesn't exist anymore. This also gives the designer the power to decide what to render in a case where the queried site doesn't exist anymore.
|
||||
|
||||
#### Available block helpers
|
||||
|
||||
Besides the available tokens above, nearly 150 [block helpers](http://handlebarsjs.com/block_helpers.html) are also available for use in the Handlebars template, see [handlebars-helpers](https://github.com/helpers/handlebars-helpers#helpers) for a list of all available block helpers.
|
||||
|
||||
*Example using the "compare" block helper for conditional rendering based on current language :*
|
||||
```handlebars
|
||||
{{#compare pageContext.web.language '==' 1033}}
|
||||
<h1>This is rendered if current language is 1033</h1>
|
||||
{{else}}
|
||||
<h1>This is rendered if current language is anything else
|
||||
{{/compare}}
|
||||
```
|
||||
|
||||
#### Displaying items and their values
|
||||
|
||||
For displaying items and their field values, we must first iterate through the exposed **{{items}}** token using a **{{each}}** block helper :
|
||||
|
||||
*Handlebars : *
|
||||
```handlebars
|
||||
{{#each items}}
|
||||
<div class="item"></div>
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
*Output : *
|
||||
```handlebars
|
||||
<div class="item"></div>
|
||||
<div class="item"></div>
|
||||
<div class="item"></div>
|
||||
...
|
||||
```
|
||||
|
||||
Once we can loop within the items, we can render any field, as long as the field has been selected in the `View Fields` property of the toolpane. The Handlebars token corresponsding to a field is always the field's internal name, which is displayed in between {{brackets}} next to the field's display name in the property pane for reference.
|
||||
|
||||
*Handlebars : *
|
||||
```handlebars
|
||||
{{#each items}}
|
||||
<div class="item">
|
||||
<p>MyField value : {{MyField}}</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
*Output : *
|
||||
```handlebars
|
||||
<div class="item">[object]</div>
|
||||
<div class="item">[object]</div>
|
||||
<div class="item">[object]</div>
|
||||
```
|
||||
|
||||
We are almost there, the above code is rendering a *[object]* because the Content Query Webpart offers 3 different ways to render a field value:
|
||||
|
||||
Property | Description
|
||||
---------|---------------
|
||||
{{MyField.textValue}} | Renders the text value of the field, a more readable end-user value to use for display.
|
||||
{{MyField.htmlValue}} | Renders the HTML value of the field. For example, a *Link* field HTML value would render something like \<a href="...">My Link Field\</a>
|
||||
{{MyField.rawValue}} | Returns the raw value of the field. For example, a *Taxonomy* field raw value would return an object which contains the term wssId and it's label
|
||||
|
||||
*Handlebars : *
|
||||
```handlebars
|
||||
{{#each items}}
|
||||
<div class="item">
|
||||
<p>MyUserField text value : {{MyUserField.textValue}}</p>
|
||||
<p>MyUserField html value : {{MyUserField.htmlValue}}</p>
|
||||
<p>MyUserField raw value : {{MyUserField.rawValue}}</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
*Output : *
|
||||
```handlebars
|
||||
<div class="item">
|
||||
<p>MyUserField text value : Simon-Pierre Plante</p>
|
||||
<p>MyUserField html value : <a href="..." onclick="...">Simon-Pierre Plante</a></p>
|
||||
<p>MyUserField raw value : 26</p>
|
||||
</div>
|
||||
...
|
||||
```
|
||||
<br>
|
||||
|
||||
## Building the code
|
||||
|
||||
```bash
|
||||
git clone the repo
|
||||
npm i
|
||||
npm i -g gulp
|
||||
gulp
|
||||
```
|
||||
|
||||
This package produces the following:
|
||||
|
||||
* lib/* - intermediate-stage commonjs build artifacts
|
||||
* dist/* - the bundled script, along with other resources
|
||||
* deploy/* - all resources which should be uploaded to a CDN.
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"entry": "./lib/webparts/contentQuery/ContentQueryWebPart.js",
|
||||
"manifest": "./src/webparts/contentQuery/ContentQueryWebPart.manifest.json",
|
||||
"outputPath": "./dist/content-query.bundle.js"
|
||||
}
|
||||
],
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"contentQueryStrings": "webparts/contentQuery/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-content-query",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"solution": {
|
||||
"name": "React Content Query",
|
||||
"id": "00406271-0276-406f-9666-512623eb6709",
|
||||
"version": "1.0.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-content-query-webpart.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"port": 4321,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"https": true,
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
// 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-unused-imports": 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,3 @@
|
|||
{
|
||||
"cdnBasePath": "https://publiccdn.sharepointonline.com/spptechnologies.sharepoint.com/110700492eeea162ee5bad0f35b1f0061ded8bf436ce0199efe2a4d24109e1c0df1ec594/react-content-query-1.0.2"
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
/********************************************************************************************
|
||||
* Adds an alias for handlebars in order to avoid errors while gulping the project
|
||||
* https://github.com/wycats/handlebars.js/issues/1174
|
||||
* Adds a loader and a node setting for webpacking the handlebars-helpers correctly
|
||||
* https://github.com/helpers/handlebars-helpers/issues/263
|
||||
********************************************************************************************/
|
||||
build.configureWebpack.mergeConfig({
|
||||
additionalConfiguration: (generatedConfiguration) => {
|
||||
|
||||
generatedConfiguration.resolve.alias = { handlebars: 'handlebars/dist/handlebars.min.js' };
|
||||
|
||||
generatedConfiguration.module.rules.push(
|
||||
{ test: /\.js$/, loader: 'unlazy-loader' }
|
||||
);
|
||||
|
||||
generatedConfiguration.node = {
|
||||
fs: 'empty'
|
||||
}
|
||||
|
||||
return generatedConfiguration;
|
||||
}
|
||||
});
|
||||
|
||||
build.initialize(gulp);
|
Binary file not shown.
After Width: | Height: | Size: 715 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "react-content-query",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "~1.1.0",
|
||||
"@microsoft/sp-webpart-base": "~1.1.1",
|
||||
"@types/handlebars": "4.0.32",
|
||||
"@types/react": "0.14.46",
|
||||
"@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",
|
||||
"handlebars": "^4.0.6",
|
||||
"handlebars-helpers": "^0.8.2",
|
||||
"moment": "^2.18.1",
|
||||
"react": "15.4.2",
|
||||
"react-ace": "^5.1.0",
|
||||
"react-dom": "15.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "~1.1.0",
|
||||
"@microsoft/sp-module-interfaces": "~1.1.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.1.0",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0",
|
||||
"awesome-typescript-loader": "^3.2.1",
|
||||
"gulp": "~3.9.1",
|
||||
"unlazy-loader": "^0.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,18 @@
|
|||
|
||||
export class ContentQueryConstants {
|
||||
|
||||
/**************************************************************
|
||||
* WebPart Properties
|
||||
**************************************************************/
|
||||
public static readonly propertyWebUrl = "webUrl";
|
||||
public static readonly propertyListTitle = "listTitle";
|
||||
public static readonly propertyOrderBy = "orderBy";
|
||||
public static readonly propertOrderByDirection = "orderByDirection";
|
||||
public static readonly propertyLimitEnabled = "limitEnabled";
|
||||
public static readonly propertyItemLimit = "itemLimit";
|
||||
public static readonly propertyFilters = "filters";
|
||||
public static readonly propertyViewFields = "viewFields";
|
||||
public static readonly propertyTemplateText = "templateText";
|
||||
public static readonly propertyTemplateUrl = "templateUrl";
|
||||
public static readonly propertyhasDefaultTemplateBeenUpdated = "hasDefaultTemplateBeenUpdated";
|
||||
}
|
|
@ -0,0 +1,296 @@
|
|||
import * as moment from 'moment';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import { IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
import { IQuerySettings } from '../../webparts/contentQuery/components/IQuerySettings';
|
||||
import { IQueryFilter } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilter';
|
||||
import { QueryFilterOperator } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/QueryFilterOperator';
|
||||
import { QueryFilterJoin } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/QueryFilterJoin';
|
||||
import { QueryFilterFieldType } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/QueryFilterFieldType';
|
||||
|
||||
export class CamlQueryHelper {
|
||||
|
||||
/*************************************************************************************************
|
||||
* Generates a full CAML query based on the provided IQuerySettings
|
||||
* @param querySettings : A IQuerySettings object required for generating the CAML query
|
||||
*************************************************************************************************/
|
||||
public static generateCamlQuery(querySettings:IQuerySettings): string {
|
||||
let query = '';
|
||||
|
||||
// Generates the <Where /> part
|
||||
if(querySettings.filters && !isEmpty(querySettings.filters)) {
|
||||
let sortedFilters = querySettings.filters.sort((a, b) => { if(a.index > b.index) { return 1; } else { return 0; } });
|
||||
query += Text.format('<Where>{0}</Where>', this.generateFilters(sortedFilters));
|
||||
}
|
||||
|
||||
// Generates the <OrderBy /> part
|
||||
if(querySettings.orderBy && !isEmpty(querySettings.orderBy)) {
|
||||
let isAscending = querySettings.orderByDirection == 'desc' ? 'FALSE' : 'TRUE';
|
||||
query += Text.format("<OrderBy><FieldRef Name='{0}' Ascending='{1}' /></OrderBy>", querySettings.orderBy, isAscending);
|
||||
}
|
||||
|
||||
// Wraps the <Where /> and <OrderBy /> into a <Query /> tag
|
||||
query = Text.format('<Query>{0}</Query>', query);
|
||||
|
||||
// Generates the <RowLimit /> part
|
||||
if(querySettings.limitEnabled) {
|
||||
query += Text.format('<RowLimit>{0}</RowLimit>', querySettings.itemLimit);
|
||||
}
|
||||
|
||||
// Generates the <ViewFields /> part
|
||||
if(querySettings.viewFields && !isEmpty(querySettings.viewFields)) {
|
||||
query += Text.format('<ViewFields>{0}</ViewFields>', querySettings.viewFields.map(field => Text.format("<FieldRef Name='{0}' />", field)).join(''));
|
||||
}
|
||||
|
||||
// Wraps the everything into a final <View /> tag
|
||||
query = Text.format('<View>{0}</View>', query);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Generates the CAML filters based on the specified array of IQueryFilter objects
|
||||
* @param filters : The filters that needs to be converted to a CAML string
|
||||
*************************************************************************************************/
|
||||
private static generateFilters(filters:IQueryFilter[]): string {
|
||||
|
||||
// Store the generic filter format for later use
|
||||
let query = '';
|
||||
let filterXml = '<{0}><FieldRef Name="{1}" /><Value {2} Type="{3}">{4}</Value></{0}>';
|
||||
|
||||
// Appends a CAML node for each filter
|
||||
let itemCount = 0;
|
||||
|
||||
for(let filter of filters.reverse()) {
|
||||
itemCount++;
|
||||
let specialAttribute = '';
|
||||
|
||||
// Sets the special attribute if needed
|
||||
if(filter.field.type == QueryFilterFieldType.Datetime) {
|
||||
specialAttribute = 'IncludeTimeValue="' + filter.includeTime + '"';
|
||||
}
|
||||
|
||||
// If it's a <IsNull /> or <IsNotNull> filter
|
||||
if(filter.operator == QueryFilterOperator.IsNull || filter.operator == QueryFilterOperator.IsNotNull) {
|
||||
filterXml = '<{0}><FieldRef Name="{1}" /></{0}>';
|
||||
query += Text.format(filterXml, QueryFilterOperator[filter.operator], filter.field.internalName);
|
||||
}
|
||||
|
||||
// If it's a taxonomy filter
|
||||
else if (filter.field.type == QueryFilterFieldType.Taxonomy) {
|
||||
query += this.generateTaxonomyFilter(filter);
|
||||
}
|
||||
|
||||
// If it's a user filter
|
||||
else if (filter.field.type == QueryFilterFieldType.User) {
|
||||
query += this.generateUserFilter(filter);
|
||||
}
|
||||
|
||||
// If it's any other kind of filter (Text, DateTime, Lookup, Number etc...)
|
||||
else {
|
||||
let valueType = (filter.field.type == QueryFilterFieldType.Lookup ? QueryFilterFieldType[QueryFilterFieldType.Text] : QueryFilterFieldType[filter.field.type]);
|
||||
query += Text.format(filterXml, QueryFilterOperator[filter.operator], filter.field.internalName, specialAttribute, valueType, this.formatFilterValue(filter));
|
||||
}
|
||||
|
||||
// Appends the Join tags if needed
|
||||
if (itemCount >= 2) {
|
||||
let logicalJoin = QueryFilterJoin[filter.join];
|
||||
query = Text.format("<{0}>", logicalJoin) + query;
|
||||
query += Text.format("</{0}>", logicalJoin);
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Generates a valid CAML filter string based on the specified taxonomy filter
|
||||
* @param filter : The taxonomy filter that needs to be formatted into a CAML filter string
|
||||
*************************************************************************************************/
|
||||
private static generateTaxonomyFilter(filter:IQueryFilter): string
|
||||
{
|
||||
let filterOutput = '';
|
||||
let filterTerms = filter.value as ITag[];
|
||||
|
||||
if(isEmpty(filter.value)) {
|
||||
return '';
|
||||
}
|
||||
else if (filter.operator == QueryFilterOperator.ContainsAny || filterTerms == null) {
|
||||
let values = filterTerms != null ? filterTerms.map(x => Text.format("<Value Type='Integer'>{0}</Value>", x.key)).join('') : '';
|
||||
filterOutput = Text.format("<In><FieldRef Name='{0}' LookupId='TRUE' /><Values>{1}</Values></In>", filter.field.internalName, values);
|
||||
}
|
||||
else if (filter.operator == QueryFilterOperator.ContainsAll) {
|
||||
let taxFilters: IQueryFilter[] = [];
|
||||
|
||||
for(let term of filterTerms) {
|
||||
let termValue:ITag[] = [ term ];
|
||||
|
||||
let taxFilter:IQueryFilter = {
|
||||
index: null,
|
||||
field: filter.field,
|
||||
value: termValue,
|
||||
join: QueryFilterJoin.And,
|
||||
operator: QueryFilterOperator.ContainsAny
|
||||
};
|
||||
taxFilters.push(taxFilter);
|
||||
}
|
||||
|
||||
filterOutput = this.generateFilters(taxFilters);
|
||||
}
|
||||
|
||||
return filterOutput;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Generates a valid CAML filter string based on the specified user filter
|
||||
* @param filter : The user filter that needs to be formatted into a CAML filter string
|
||||
*************************************************************************************************/
|
||||
private static generateUserFilter(filter:IQueryFilter): string
|
||||
{
|
||||
let filterOutput = '';
|
||||
let filterUsers = filter.value as IPersonaProps[];
|
||||
|
||||
if(filter.me) {
|
||||
filterOutput = Text.format("<Eq><FieldRef Name='{0}' /><Value Type='Integer'><UserID /></Value></Eq>", filter.field.internalName);
|
||||
}
|
||||
else if(isEmpty(filter.value)) {
|
||||
return '';
|
||||
}
|
||||
else if (filter.operator == QueryFilterOperator.ContainsAny || filterUsers == null)
|
||||
{
|
||||
let values = filterUsers != null ? filterUsers.map(x => Text.format("<Value Type='Integer'>{0}</Value>", x.value)).join('') : '';
|
||||
filterOutput = Text.format("<In><FieldRef Name='{0}' LookupId='TRUE' /><Values>{1}</Values></In>", filter.field.internalName, values);
|
||||
}
|
||||
else if (filter.operator == QueryFilterOperator.ContainsAll)
|
||||
{
|
||||
let userFilters: IQueryFilter[] = [];
|
||||
|
||||
for(let user of filterUsers) {
|
||||
let userValue:IPersonaProps[] = [ user ];
|
||||
|
||||
let userFilter:IQueryFilter = {
|
||||
index: null,
|
||||
field: filter.field,
|
||||
value: userValue,
|
||||
join: QueryFilterJoin.And,
|
||||
operator: QueryFilterOperator.ContainsAny
|
||||
};
|
||||
userFilters.push(userFilter);
|
||||
}
|
||||
|
||||
filterOutput = this.generateFilters(userFilters);
|
||||
}
|
||||
|
||||
return filterOutput;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Returns the value of the specified filter correctly formatted based on its type of value
|
||||
* @param filter : The filter that needs its value to be formatted
|
||||
*************************************************************************************************/
|
||||
private static formatFilterValue(filter:IQueryFilter): string
|
||||
{
|
||||
let filterValue = "";
|
||||
|
||||
if(filter.field.type == QueryFilterFieldType.Datetime) {
|
||||
if(filter.expression != null && !isEmpty(filter.expression)) {
|
||||
filterValue = this.formatDateExpressionFilterValue(filter.expression);
|
||||
}
|
||||
else {
|
||||
filterValue = this.formatDateFilterValue(filter.value as string);
|
||||
}
|
||||
}
|
||||
else {
|
||||
filterValue = this.formatTextFilterValue(filter.value as string);
|
||||
}
|
||||
|
||||
return filterValue;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Converts the specified serialized ISO date into the required string format
|
||||
* @param dateValue : A valid ISO 8601 date string
|
||||
*************************************************************************************************/
|
||||
private static formatDateFilterValue(dateValue:string): string {
|
||||
let date = moment(dateValue, moment.ISO_8601, true);
|
||||
|
||||
if(date.isValid()) {
|
||||
dateValue = date.format("YYYY-MM-DDTHH:mm:ss\\Z");
|
||||
}
|
||||
return dateValue || '';
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Replaces any "[Today]" or "[Today] +/- [digit]" expression by it's actual value
|
||||
* @param filterValue : The filter value
|
||||
*************************************************************************************************/
|
||||
private static formatDateExpressionFilterValue(filterValue: string): string {
|
||||
|
||||
// Replaces any "[Today] +/- [digit]" expression
|
||||
let regex = new RegExp("\\[Today\\]\\s*[\\+-]\\s*\\[{0,1}\\d{1,}\\]{0,1}");
|
||||
let results = regex.exec(filterValue);
|
||||
|
||||
if(results != null) {
|
||||
for(let result of results) {
|
||||
let operator = result.indexOf('+') > 0 ? '+' : '-';
|
||||
let addOrRemove = operator == '+' ? 1 : -1;
|
||||
let operatorSplit = result.split(operator);
|
||||
let digit = parseInt(operatorSplit[operatorSplit.length - 1].replace("[", "").replace("]", "").trim()) * addOrRemove;
|
||||
let dt = new Date();
|
||||
dt.setDate(dt.getDate() + digit);
|
||||
let formattedDate = moment(dt).format("YYYY-MM-DDTHH:mm:ss\\Z");
|
||||
filterValue = filterValue.replace(result, formattedDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces any "[Today]" expression by it's actual value
|
||||
let formattedDate = moment(new Date()).format("YYYY-MM-DDTHH:mm:ss\\Z");
|
||||
filterValue = filterValue.replace("[Today]", formattedDate);
|
||||
|
||||
return filterValue;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Formats the specified text filter value
|
||||
* @param textValue : The text filter value which needs to be formatted
|
||||
*************************************************************************************************/
|
||||
private static formatTextFilterValue(textValue:string): string {
|
||||
let regex = new RegExp("\\[PageQueryString:[A-Za-z0-9_-]*\\]");
|
||||
let results = regex.exec(textValue);
|
||||
|
||||
if(results != null) {
|
||||
for(let result of results) {
|
||||
let parameter = result.substring(17, result.length - 1);
|
||||
textValue = textValue.replace(result, this.getUrlParameter(parameter));
|
||||
}
|
||||
}
|
||||
|
||||
return textValue != null ? textValue : '';
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Returns the value of the query string parameter with the specified name
|
||||
* @param name : The name of the query string parameter
|
||||
* @param url : Optionnaly, the specific url to use instead of the current url
|
||||
*************************************************************************************************/
|
||||
private static getUrlParameter(name: string, url?: string): string {
|
||||
if (!url) {
|
||||
url = window.location.href;
|
||||
}
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,608 @@
|
|||
import * as strings from 'contentQueryStrings';
|
||||
import { IDropdownOption, IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { Text, Log } from '@microsoft/sp-core-library';
|
||||
import { IContentQueryService } from './IContentQueryService';
|
||||
import { IQueryFilterField } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilterField';
|
||||
import { QueryFilterFieldType } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/QueryFilterFieldType';
|
||||
import { IChecklistItem } from '../../controls/PropertyPaneAsyncChecklist/components/AsyncChecklist/IChecklistItem';
|
||||
import { IContentQueryTemplateContext } from '../../webparts/contentQuery/components/IContentQueryTemplateContext';
|
||||
import { IQuerySettings } from '../../webparts/contentQuery/components/IQuerySettings';
|
||||
import { CamlQueryHelper } from '../helpers/CamlQueryHelper';
|
||||
import { ListService } from './ListService';
|
||||
import { SearchService } from './SearchService';
|
||||
import { PeoplePickerService } from './PeoplePickerService';
|
||||
import { TaxonomyService } from './TaxonomyService';
|
||||
|
||||
|
||||
export class ContentQueryService implements IContentQueryService {
|
||||
|
||||
private readonly logSource = "ContentQueryService.ts";
|
||||
|
||||
/***************************************************************************
|
||||
* The page context and http clients used for performing REST calls
|
||||
***************************************************************************/
|
||||
private context: IWebPartContext;
|
||||
private spHttpClient: SPHttpClient;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* The different services used to perform REST calls
|
||||
***************************************************************************/
|
||||
private listService: ListService;
|
||||
private searchService: SearchService;
|
||||
private peoplePickerService: PeoplePickerService;
|
||||
private taxonomyService: TaxonomyService;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Stores the first async calls locally to avoid useless redundant calls
|
||||
***************************************************************************/
|
||||
private webUrlOptions: IDropdownOption[];
|
||||
private listTitleOptions: IDropdownOption[];
|
||||
private orderByOptions: IDropdownOption[];
|
||||
private filterFields: IQueryFilterField[];
|
||||
private viewFields: IChecklistItem[];
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Constructor
|
||||
* @param context : A IWebPartContext for logging and page context
|
||||
* @param spHttpClient : A SPHttpClient for performing SharePoint specific requests
|
||||
***************************************************************************/
|
||||
constructor(context: IWebPartContext, spHttpClient: SPHttpClient) {
|
||||
Log.verbose(this.logSource, "Initializing a new IContentQueryService instance...", context.serviceScope);
|
||||
|
||||
this.context = context;
|
||||
this.spHttpClient = spHttpClient;
|
||||
this.listService = new ListService(this.spHttpClient);
|
||||
this.searchService = new SearchService(this.spHttpClient);
|
||||
this.peoplePickerService = new PeoplePickerService(this.spHttpClient);
|
||||
this.taxonomyService = new TaxonomyService(this.spHttpClient);
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Gets the available webs for the current user
|
||||
**************************************************************************************************/
|
||||
public getTemplateContext(querySettings: IQuerySettings, callTimeStamp: number): Promise<IContentQueryTemplateContext> {
|
||||
Log.verbose(this.logSource, Text.format("Getting template context for request with queue number {0}...", callTimeStamp), this.context.serviceScope);
|
||||
|
||||
return new Promise<IContentQueryTemplateContext>((resolve,reject) => {
|
||||
|
||||
// Initializes the base template context
|
||||
let templateContext:IContentQueryTemplateContext = {
|
||||
pageContext: this.context.pageContext,
|
||||
items: [],
|
||||
accessDenied: false,
|
||||
webNotFound: false,
|
||||
callTimeStamp: callTimeStamp
|
||||
};
|
||||
|
||||
// Builds the CAML query based on the webpart settings
|
||||
let query = CamlQueryHelper.generateCamlQuery(querySettings);
|
||||
Log.info(this.logSource, Text.format("Generated CAML query {0}...", query), this.context.serviceScope);
|
||||
|
||||
// Queries the list with the generated caml query
|
||||
this.listService.getListItemsByQuery(querySettings.webUrl, querySettings.listTitle, query)
|
||||
.then((data: any) => {
|
||||
// Updates the template context with the normalized query results
|
||||
let normalizedResults = this.normalizeQueryResults(data.value, querySettings.viewFields);
|
||||
templateContext.items = normalizedResults;
|
||||
resolve(templateContext);
|
||||
})
|
||||
.catch((error) => {
|
||||
// If it fails because previously configured web/list isn't accessible for current user
|
||||
if(error.status === 403) {
|
||||
|
||||
// Still resolve with accessDenied=true so the handlebar template can decide what to render in that case
|
||||
templateContext.accessDenied = true;
|
||||
resolve(templateContext);
|
||||
}
|
||||
|
||||
// If it fails because previously configured web/list doesn't exist anymore
|
||||
else if(error.status === 404) {
|
||||
|
||||
// Still resolve with webNotFound=true so the handlebar template can decide what to render in that case
|
||||
templateContext.webNotFound = true;
|
||||
resolve(templateContext);
|
||||
}
|
||||
|
||||
// If it fails for any other reason, reject with the error message
|
||||
else {
|
||||
let errorMessage: string = error.statusText ? error.statusText : error;
|
||||
reject(errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Executes an HTTP request against the specified file and returns a promise with it's content
|
||||
* @param fileUrl : The url of the file
|
||||
**************************************************************************************************/
|
||||
public getFileContent(fileUrl: string): Promise<string> {
|
||||
Log.verbose(this.logSource, Text.format("Getting content for file with url '{0}'...", fileUrl), this.context.serviceScope);
|
||||
|
||||
return new Promise<string>((resolve,reject) => {
|
||||
this.spHttpClient.get(fileUrl, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
resolve(response.text());
|
||||
}
|
||||
else {
|
||||
reject(response.statusText);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Gets the available webs for the current user
|
||||
**************************************************************************************************/
|
||||
public getWebUrlOptions(): Promise<IDropdownOption[]> {
|
||||
Log.verbose(this.logSource, "Loading dropdown options for toolpart property 'Web Url'...", this.context.serviceScope);
|
||||
|
||||
// Resolves the already loaded data if available
|
||||
if(this.webUrlOptions) {
|
||||
return Promise.resolve(this.webUrlOptions);
|
||||
}
|
||||
|
||||
// Otherwise, performs a REST call to get the data
|
||||
return new Promise<IDropdownOption[]>((resolve,reject) => {
|
||||
let serverUrl = Text.format("{0}//{1}", window.location.protocol, window.location.hostname);
|
||||
|
||||
this.searchService.getWebUrlsForDomain(serverUrl)
|
||||
.then((urls:string[]) => {
|
||||
let options:IDropdownOption[] = [ { key: "", text: strings.WebUrlFieldPlaceholder } ];
|
||||
let urlOptions:IDropdownOption[] = urls.sort().map((url) => {
|
||||
let serverRelativeUrl = !isEmpty(url.replace(serverUrl, '')) ? url.replace(serverUrl, '') : '/';
|
||||
return { key: url, text: serverRelativeUrl };
|
||||
});
|
||||
options = options.concat(urlOptions);
|
||||
this.webUrlOptions = options;
|
||||
resolve(options);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Gets the available lists from the specified web
|
||||
* @param webUrl : The url of the web from which lists must be loaded from
|
||||
**************************************************************************************************/
|
||||
public getListTitleOptions(webUrl: string): Promise<IDropdownOption[]> {
|
||||
Log.verbose(this.logSource, "Loading dropdown options for toolpart property 'List Title'...", this.context.serviceScope);
|
||||
|
||||
// Resolves an empty array if web is null
|
||||
if (isEmpty(webUrl)) {
|
||||
return Promise.resolve(new Array<IDropdownOption>());
|
||||
}
|
||||
|
||||
// Resolves the already loaded data if available
|
||||
if(this.listTitleOptions) {
|
||||
return Promise.resolve(this.listTitleOptions);
|
||||
}
|
||||
|
||||
// Otherwise gets the options asynchronously
|
||||
return new Promise<IDropdownOption[]>((resolve, reject) => {
|
||||
this.listService.getListTitlesFromWeb(webUrl).then((listTitles:string[]) => {
|
||||
let options:IDropdownOption[] = [ { key: "", text: strings.ListTitleFieldPlaceholder } ];
|
||||
let listTitleOptions = listTitles.map((listTitle) => { return { key: listTitle, text: listTitle }; });
|
||||
options = options.concat(listTitleOptions);
|
||||
this.listTitleOptions = options;
|
||||
resolve(options);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(this.getErrorMessage(webUrl, error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Gets the available fields out of the specified web/list
|
||||
* @param webUrl : The url of the web from which the list comes from
|
||||
* @param listTitle : The title of the list from which the field must be loaded from
|
||||
**************************************************************************************************/
|
||||
public getOrderByOptions(webUrl: string, listTitle: string): Promise<IDropdownOption[]> {
|
||||
Log.verbose(this.logSource, "Loading dropdown options for toolpart property 'Order By'...", this.context.serviceScope);
|
||||
|
||||
// Resolves an empty array if no web or no list has been selected
|
||||
if (isEmpty(webUrl) || isEmpty(listTitle)) {
|
||||
return Promise.resolve(new Array<IDropdownOption>());
|
||||
}
|
||||
|
||||
// Resolves the already loaded data if available
|
||||
if(this.orderByOptions) {
|
||||
return Promise.resolve(this.orderByOptions);
|
||||
}
|
||||
|
||||
// Otherwise gets the options asynchronously
|
||||
return new Promise<IDropdownOption[]>((resolve, reject) => {
|
||||
this.listService.getListFields(webUrl, listTitle, ['InternalName', 'Title', 'Sortable'], 'Title').then((data:any) => {
|
||||
let sortableFields:any[] = data.value.filter((field) => { return field.Sortable == true; });
|
||||
let options:IDropdownOption[] = [ { key: "", text: strings.queryFilterPanelStrings.queryFilterStrings.fieldSelectLabel } ];
|
||||
let orderByOptions:IDropdownOption[] = sortableFields.map((field) => { return { key: field.InternalName, text: Text.format("{0} \{\{{1}\}\}", field.Title, field.InternalName) }; });
|
||||
options = options.concat(orderByOptions);
|
||||
this.orderByOptions = options;
|
||||
resolve(options);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(this.getErrorMessage(webUrl, error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Gets the available fields out of the specified web/list
|
||||
* @param webUrl : The url of the web from which the list comes from
|
||||
* @param listTitle : The title of the list from which the field must be loaded from
|
||||
***************************************************************************/
|
||||
public getFilterFields(webUrl: string, listTitle: string):Promise<IQueryFilterField[]> {
|
||||
Log.verbose(this.logSource, "Loading dropdown options for toolpart property 'Filters'...", this.context.serviceScope);
|
||||
|
||||
// Resolves an empty array if no web or no list has been selected
|
||||
if (isEmpty(webUrl) || isEmpty(listTitle)) {
|
||||
return Promise.resolve(new Array<IQueryFilterField>());
|
||||
}
|
||||
|
||||
// Resolves the already loaded data if available
|
||||
if(this.filterFields) {
|
||||
return Promise.resolve(this.filterFields);
|
||||
}
|
||||
|
||||
// Otherwise gets the options asynchronously
|
||||
return new Promise<IQueryFilterField[]>((resolve, reject) => {
|
||||
this.listService.getListFields(webUrl, listTitle, ['InternalName', 'Title', 'TypeAsString'], 'Title').then((data:any) => {
|
||||
let fields:any[] = data.value;
|
||||
let options:IQueryFilterField[] = fields.map((field) => { return {
|
||||
internalName: field.InternalName,
|
||||
displayName: field.Title,
|
||||
type: this.getFieldTypeFromString(field.TypeAsString)
|
||||
}; });
|
||||
this.filterFields = options;
|
||||
resolve(options);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(this.getErrorMessage(webUrl, error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the checklist items for the viewFields property
|
||||
***************************************************************************/
|
||||
public getViewFieldsChecklistItems(webUrl: string, listTitle: string):Promise<IChecklistItem[]> {
|
||||
Log.verbose(this.logSource, "Loading checklist items for toolpart property 'View Fields'...", this.context.serviceScope);
|
||||
|
||||
// Resolves an empty array if no web or no list has been selected
|
||||
if (isEmpty(webUrl) || isEmpty(listTitle)) {
|
||||
return Promise.resolve(new Array<IChecklistItem>());
|
||||
}
|
||||
|
||||
// Resolves the already loaded data if available
|
||||
if(this.viewFields) {
|
||||
return Promise.resolve(this.viewFields);
|
||||
}
|
||||
|
||||
// Otherwise gets the options asynchronously
|
||||
return new Promise<IChecklistItem[]>((resolve, reject) => {
|
||||
this.listService.getListFields(webUrl, listTitle, ['InternalName', 'Title'], 'Title').then((data:any) => {
|
||||
let fields:any[] = data.value;
|
||||
let items:IChecklistItem[] = fields.map((field) => { return {
|
||||
id: field.InternalName,
|
||||
label: Text.format("{0} \{\{{1}\}\}", field.Title, field.InternalName)
|
||||
}; });
|
||||
this.viewFields = items;
|
||||
resolve(items);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(this.getErrorMessage(webUrl, error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the user suggestions based on the user entered picker input
|
||||
* @param webUrl : The web url on which to query for users
|
||||
* @param filterText : The filter specified by the user in the people picker
|
||||
* @param currentPersonas : The IPersonaProps already selected in the people picker
|
||||
* @param limitResults : The results limit if any
|
||||
***************************************************************************/
|
||||
public getPeoplePickerSuggestions(webUrl: string, filterText: string, currentPersonas: IPersonaProps[], limitResults?: number):Promise<IPersonaProps[]> {
|
||||
Log.verbose(this.logSource, "Getting people picker suggestions for toolpart property 'Filters'...", this.context.serviceScope);
|
||||
|
||||
return new Promise<IPersonaProps[]>((resolve, reject) => {
|
||||
this.peoplePickerService.getUserSuggestions(webUrl, filterText, 1, 15, limitResults).then((data) => {
|
||||
let users: any[] = JSON.parse(data.value);
|
||||
let userSuggestions:IPersonaProps[] = users.map((user) => { return {
|
||||
primaryText: user.DisplayText,
|
||||
value: user.EntityData.SPUserID || user.EntityData.SPGroupID
|
||||
}; });
|
||||
resolve(this.removeUserSuggestionsDuplicates(userSuggestions, currentPersonas));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the taxonomy suggestions based on the user entered picker input
|
||||
* @param webUrl : The web url on which to query for users
|
||||
* @param filterText : The filter specified by the user in the people picker
|
||||
* @param currentPersonas : The IPersonaProps already selected in the people picker
|
||||
* @param limitResults : The results limit if any
|
||||
***************************************************************************/
|
||||
public getTaxonomyPickerSuggestions(webUrl: string, listTitle: string, field: IQueryFilterField, filterText: string, currentTerms: ITag[]):Promise<ITag[]> {
|
||||
Log.verbose(this.logSource, "Getting taxonomy picker suggestions for toolpart property 'Filters'...", this.context.serviceScope);
|
||||
|
||||
return new Promise<ITag[]>((resolve, reject) => {
|
||||
this.taxonomyService.getSiteTaxonomyTermsByTermSet(webUrl, listTitle, field.internalName, this.context.pageContext.web.language).then((data:any) => {
|
||||
let termField = Text.format('Term{0}', this.context.pageContext.web.language);
|
||||
let terms: any[] = data.value;
|
||||
let termSuggestions: ITag[] = terms.map((term:any) => { return { key: term.Id, name: term[termField] }; });
|
||||
resolve(this.removeTermSuggestionsDuplicates(termSuggestions, currentTerms));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Performs a GET request against the specified file path and returns whether it resolved or not
|
||||
* @param filePath : The path of the file that needs to be validated against a HEAD request
|
||||
*************************************************************************************************/
|
||||
public ensureFileResolves(filePath: string): Promise<{}> {
|
||||
Log.verbose(this.logSource, Text.format("Checking if file exists at url '{0}'...", filePath), this.context.serviceScope);
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
this.spHttpClient.get(filePath, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
resolve();
|
||||
}
|
||||
else {
|
||||
reject(response.statusText);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Returns whether the specified file path is a valid .htm or .html filePath
|
||||
* @param filePath : The path of the file which needs to be validated
|
||||
*************************************************************************************************/
|
||||
public isValidTemplateFile(filePath: string): boolean {
|
||||
Log.verbose(this.logSource, Text.format("Validating template file at url '{0}'...", filePath), this.context.serviceScope);
|
||||
|
||||
let path = filePath.toLowerCase().trim();
|
||||
let pathExtension = path.substring(path.lastIndexOf('.'));
|
||||
return (pathExtension == '.htm' || pathExtension == '.html');
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************************
|
||||
* Generates a default handlebars template based on the view fields selected by the user
|
||||
* @param viewFields : The view fields that have been selected by the user
|
||||
*************************************************************************************************/
|
||||
public generateDefaultTemplate(viewFields: string[]): string {
|
||||
let viewFieldsStr = viewFields.map((field) => { return Text.format(" <span><b>{0} : </b>\{\{{0}.textValue\}\}</span>", field); }).join("\n");
|
||||
let template = Text.format(`<style type="text/css">
|
||||
.dynamic-template .dynamic-items .dynamic-item {
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 0px 6px #bfbebe;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.dynamic-template .dynamic-items .dynamic-item h3 {
|
||||
background: #47b4de;
|
||||
color: #fff;
|
||||
padding: 5px 5px 7px 10px;
|
||||
margin: 0px;
|
||||
}
|
||||
.dynamic-template .dynamic-items .dynamic-item .dynamic-item-fields {
|
||||
padding: 10px;
|
||||
}
|
||||
.dynamic-template .dynamic-items .dynamic-item .dynamic-item-fields span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="dynamic-template">
|
||||
<h2>{0}</h2>
|
||||
<div class="dynamic-items">
|
||||
{{#each items}}
|
||||
<div class="dynamic-item">
|
||||
<h3>Result #{{@index}}</h3>
|
||||
<div class="dynamic-item-fields">
|
||||
{1}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>`, strings.DynamicallyGeneratedTemplate ,viewFieldsStr);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the stored 'list title' options
|
||||
***************************************************************************/
|
||||
public clearCachedListTitleOptions() {
|
||||
Log.verbose(this.logSource, "Clearing cached dropdown options for toolpart property 'List Title'...", this.context.serviceScope);
|
||||
this.listTitleOptions = null;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the stored 'order by' options
|
||||
***************************************************************************/
|
||||
public clearCachedOrderByOptions() {
|
||||
Log.verbose(this.logSource, "Clearing cached dropdown options for toolpart property 'Order By'...", this.context.serviceScope);
|
||||
this.orderByOptions = null;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the stored filter fields
|
||||
***************************************************************************/
|
||||
public clearCachedFilterFields() {
|
||||
Log.verbose(this.logSource, "Clearing cached dropdown options for toolpart property 'Filter'...", this.context.serviceScope);
|
||||
this.filterFields = null;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the stored view fields
|
||||
***************************************************************************/
|
||||
public clearCachedViewFields() {
|
||||
Log.verbose(this.logSource, "Clearing cached checklist items for toolpart property 'View Fields'...", this.context.serviceScope);
|
||||
this.viewFields = null;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Normalizes the results coming from a CAML query into a userfriendly format for handlebars
|
||||
* @param results : The results returned by a CAML query executed against a list
|
||||
***************************************************************************/
|
||||
private normalizeQueryResults(results: any[], viewFields: string[]): any[] {
|
||||
Log.verbose(this.logSource, "Normalizing results for the requested handlebars context...", this.context.serviceScope);
|
||||
|
||||
let normalizedResults: any[] = [];
|
||||
|
||||
for(let result of results) {
|
||||
let normalizedResult: any = {};
|
||||
|
||||
for(let viewField of viewFields) {
|
||||
normalizedResult[viewField] = {
|
||||
textValue: result.FieldValuesAsText[viewField],
|
||||
htmlValue: result.FieldValuesAsHtml[viewField],
|
||||
rawValue: result[viewField] || result[viewField + 'Id']
|
||||
};
|
||||
}
|
||||
normalizedResults.push(normalizedResult);
|
||||
}
|
||||
return normalizedResults;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns an error message based on the specified error object
|
||||
* @param error : An error string/object
|
||||
***************************************************************************/
|
||||
private getErrorMessage(webUrl: string, error: any): string {
|
||||
let errorMessage:string = error.statusText ? error.statusText : error;
|
||||
let serverUrl = Text.format("{0}//{1}", window.location.protocol, window.location.hostname);
|
||||
let webServerRelativeUrl = webUrl.replace(serverUrl, '');
|
||||
|
||||
if(error.status === 403) {
|
||||
errorMessage = Text.format(strings.ErrorWebAccessDenied, webServerRelativeUrl);
|
||||
}
|
||||
else if(error.status === 404) {
|
||||
errorMessage = Text.format(strings.ErrorWebNotFound, webServerRelativeUrl);
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns a field type enum value based on the provided string type
|
||||
* @param fieldTypeStr : The field type as a string
|
||||
***************************************************************************/
|
||||
private getFieldTypeFromString(fieldTypeStr: string): QueryFilterFieldType {
|
||||
let fieldType:QueryFilterFieldType;
|
||||
|
||||
switch(fieldTypeStr.toLowerCase().trim()) {
|
||||
case 'user': fieldType = QueryFilterFieldType.User;
|
||||
break;
|
||||
|
||||
case 'usermulti': fieldType = QueryFilterFieldType.User;
|
||||
break;
|
||||
|
||||
case 'datetime': fieldType= QueryFilterFieldType.Datetime;
|
||||
break;
|
||||
|
||||
case 'lookup': fieldType = QueryFilterFieldType.Lookup;
|
||||
break;
|
||||
|
||||
case 'url': fieldType = QueryFilterFieldType.Url;
|
||||
break;
|
||||
|
||||
case 'number': fieldType = QueryFilterFieldType.Number;
|
||||
break;
|
||||
|
||||
case 'taxonomyfieldtype': fieldType = QueryFilterFieldType.Taxonomy;
|
||||
break;
|
||||
|
||||
case 'taxonomyfieldtypemulti': fieldType = QueryFilterFieldType.Taxonomy;
|
||||
break;
|
||||
|
||||
default: fieldType = QueryFilterFieldType.Text;
|
||||
break;
|
||||
}
|
||||
return fieldType;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the specified users with possible duplicates removed
|
||||
* @param users : The user suggestions from which duplicates must be removed
|
||||
* @param currentUsers : The current user suggestions that could be duplicates
|
||||
***************************************************************************/
|
||||
private removeUserSuggestionsDuplicates(users: IPersonaProps[], currentUsers: IPersonaProps[]): IPersonaProps[] {
|
||||
Log.verbose(this.logSource, "Removing user suggestions duplicates for toolpart property 'Filters'...", this.context.serviceScope);
|
||||
let trimmedUsers: IPersonaProps[] = [];
|
||||
|
||||
for(let user of users) {
|
||||
let isDuplicate = currentUsers.filter((u) => { return u.value === user.value; }).length > 0;
|
||||
|
||||
if(!isDuplicate) {
|
||||
trimmedUsers.push(user);
|
||||
}
|
||||
}
|
||||
return trimmedUsers;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the specified users with possible duplicates removed
|
||||
* @param users : The user suggestions from which duplicates must be removed
|
||||
* @param currentUsers : The current user suggestions that could be duplicates
|
||||
***************************************************************************/
|
||||
private removeTermSuggestionsDuplicates(terms: ITag[], currentTerms: ITag[]): ITag[] {
|
||||
Log.verbose(this.logSource, "Removing term suggestions duplicates for toolpart property 'Filters'...", this.context.serviceScope);
|
||||
let trimmedTerms: ITag[] = [];
|
||||
|
||||
for(let term of terms) {
|
||||
let isDuplicate = currentTerms.filter((t) => { return t.key === term.key; }).length > 0;
|
||||
|
||||
if(!isDuplicate) {
|
||||
trimmedTerms.push(term);
|
||||
}
|
||||
}
|
||||
return trimmedTerms;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { IDropdownOption, IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
import { IQueryFilterField } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilterField';
|
||||
import { IChecklistItem } from '../../controls/PropertyPaneAsyncChecklist/components/AsyncChecklist/IChecklistItem';
|
||||
import { IContentQueryTemplateContext } from '../../webparts/contentQuery/components/IContentQueryTemplateContext';
|
||||
import { IQuerySettings } from '../../webparts/contentQuery/components/IQuerySettings';
|
||||
|
||||
|
||||
export interface IContentQueryService {
|
||||
getTemplateContext: (querySettings: IQuerySettings, callTimeStamp: number) => Promise<IContentQueryTemplateContext>;
|
||||
getFileContent: (fileUrl: string) => Promise<string>;
|
||||
getWebUrlOptions: () => Promise<IDropdownOption[]>;
|
||||
getListTitleOptions: (webUrl: string) => Promise<IDropdownOption[]>;
|
||||
getOrderByOptions: (webUrl: string, listTitle: string) => Promise<IDropdownOption[]>;
|
||||
getFilterFields: (webUrl: string, listTitle: string) => Promise<IQueryFilterField[]>;
|
||||
getViewFieldsChecklistItems: (webUrl: string, listTitle: string) => Promise<IChecklistItem[]>;
|
||||
getPeoplePickerSuggestions: (webUrl: string, filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) => Promise<IPersonaProps[]>;
|
||||
getTaxonomyPickerSuggestions: (webUrl: string, listTitle: string, field: IQueryFilterField, filterText: string, currentTerms: ITag[]) => Promise<ITag[]>;
|
||||
ensureFileResolves: (filePath: string) => Promise<{}>;
|
||||
isValidTemplateFile: (filePath: string) => boolean;
|
||||
generateDefaultTemplate: (viewFields: string[]) => string;
|
||||
clearCachedListTitleOptions: () => void;
|
||||
clearCachedOrderByOptions: () => void;
|
||||
clearCachedFilterFields: () => void;
|
||||
clearCachedViewFields: () => void;
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { SPHttpClient, ISPHttpClientOptions, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
|
||||
export class ListService {
|
||||
|
||||
/***************************************************************************
|
||||
* The spHttpClient object used for performing REST calls to SharePoint
|
||||
***************************************************************************/
|
||||
private spHttpClient: SPHttpClient;
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Constructor
|
||||
* @param httpClient : The spHttpClient required to perform REST calls against SharePoint
|
||||
**************************************************************************************************/
|
||||
constructor(spHttpClient: SPHttpClient) {
|
||||
this.spHttpClient = spHttpClient;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Performs a CAML query against the specified list and returns the resulting items
|
||||
* @param webUrl : The url of the web which contains the specified list
|
||||
* @param listTitle : The title of the list which contains the elements to query
|
||||
* @param camlQuery : The CAML query to perform on the specified list
|
||||
**************************************************************************************************/
|
||||
public getListItemsByQuery(webUrl: string, listTitle: string, camlQuery: string): Promise<any> {
|
||||
return new Promise<any>((resolve,reject) => {
|
||||
let endpoint = Text.format("{0}/_api/web/lists/GetByTitle('{1}')/GetItems?$expand=FieldValuesAsText,FieldValuesAsHtml", webUrl, listTitle);
|
||||
let data:any = {
|
||||
query : {
|
||||
__metadata: { type: "SP.CamlQuery" },
|
||||
ViewXml: camlQuery
|
||||
}
|
||||
};
|
||||
let options: ISPHttpClientOptions = { headers: { 'odata-version': '3.0' }, body: JSON.stringify(data) };
|
||||
|
||||
this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, options)
|
||||
.then((postResponse: SPHttpClientResponse) => {
|
||||
if(postResponse.ok) {
|
||||
resolve(postResponse.json());
|
||||
}
|
||||
else {
|
||||
reject(postResponse);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Returns a sorted array of all available list titles for the specified web
|
||||
* @param webUrl : The web URL from which the list titles must be taken from
|
||||
**************************************************************************************************/
|
||||
public getListTitlesFromWeb(webUrl: string): Promise<string[]> {
|
||||
return new Promise<string[]>((resolve,reject) => {
|
||||
let endpoint = Text.format("{0}/_api/web/lists?$select=Title&$filter=(IsPrivate eq false) and (IsCatalog eq false) and (Hidden eq false)", webUrl);
|
||||
this.spHttpClient.get(endpoint, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
response.json().then((data: any) => {
|
||||
let listTitles:string[] = data.value.map((list) => { return list.Title; });
|
||||
resolve(listTitles.sort());
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
}
|
||||
else {
|
||||
reject(response);
|
||||
}
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Returns a sorted array of all available list titles for the specified web
|
||||
* @param webUrl : The web URL from which the specified list is located
|
||||
* @param listTitle : The title of the list from which to load the fields
|
||||
* @param selectProperties : Optionnaly, the select properties to narrow down the query scope
|
||||
**************************************************************************************************/
|
||||
public getListFields(webUrl: string, listTitle: string, selectProperties?: string[], orderBy?: string): Promise<any> {
|
||||
return new Promise<any>((resolve,reject) => {
|
||||
let selectProps = selectProperties ? selectProperties.join(',') : '';
|
||||
let order = orderBy ? orderBy : 'InternalName';
|
||||
let endpoint = Text.format("{0}/_api/web/lists/GetByTitle('{1}')/Fields?$select={2}&$orderby={3}", webUrl, listTitle, selectProps, order);
|
||||
this.spHttpClient.get(endpoint, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
resolve(response.json());
|
||||
}
|
||||
else {
|
||||
reject(response);
|
||||
}
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { SPHttpClient, ISPHttpClientOptions, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
|
||||
export class PeoplePickerService {
|
||||
|
||||
/***************************************************************************
|
||||
* The spHttpClient object used for performing REST calls to SharePoint
|
||||
***************************************************************************/
|
||||
private spHttpClient: SPHttpClient;
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Constructor
|
||||
* @param httpClient : The spHttpClient required to perform REST calls against SharePoint
|
||||
**************************************************************************************************/
|
||||
constructor(spHttpClient: SPHttpClient) {
|
||||
this.spHttpClient = spHttpClient;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Performs a CAML query against the specified list and returns the resulting items
|
||||
* @param webUrl : The url of the current web
|
||||
* @param query : The query on which the user suggestions must be based on
|
||||
* @param principalSource : The source to search (15=All, 4=Membership Provider, 8=Role Provider, 1=User Info List, 2=Windows)
|
||||
* @param principalType : The type of entities returned (15=All, 2=Distribution Lists, 4=Security Groups,8=SharePoint Groups, 1=Users)
|
||||
* @param maximumEntitySuggestion : Limit the amount of returned results
|
||||
**************************************************************************************************/
|
||||
public getUserSuggestions(webUrl: string, query: string, principalSource: number, principalType: number, maximumEntitySuggestion?: number): Promise<any> {
|
||||
return new Promise<any>((resolve,reject) => {
|
||||
let endpoint = Text.format("{0}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser", webUrl);
|
||||
let data:any = {
|
||||
queryParams:{
|
||||
__metadata:{
|
||||
'type':'SP.UI.ApplicationPages.ClientPeoplePickerQueryParameters'
|
||||
},
|
||||
QueryString: query,
|
||||
PrincipalSource: principalSource,
|
||||
PrincipalType: principalType,
|
||||
MaximumEntitySuggestions: maximumEntitySuggestion || 50
|
||||
}
|
||||
};
|
||||
let options: ISPHttpClientOptions = { headers: { 'odata-version': '3.0' }, body: JSON.stringify(data) };
|
||||
|
||||
this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, options)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
resolve(response.json());
|
||||
}
|
||||
else {
|
||||
reject(response.statusText);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
|
||||
export class SearchService {
|
||||
|
||||
/***************************************************************************
|
||||
* The spHttpClient object used for performing REST calls to SharePoint
|
||||
***************************************************************************/
|
||||
private spHttpClient: SPHttpClient;
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Constructor
|
||||
* @param httpClient : The spHttpClient required to perform REST calls against SharePoint
|
||||
**************************************************************************************************/
|
||||
constructor(spHttpClient: SPHttpClient) {
|
||||
this.spHttpClient = spHttpClient;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Returns the web urls starting with the specified domain to which the current user has access
|
||||
* @param domainUrl : The url of the web which contains the specified list
|
||||
**************************************************************************************************/
|
||||
public getWebUrlsForDomain(domainUrl: string): Promise<string[]> {
|
||||
return new Promise<string[]>((resolve,reject) => {
|
||||
let endpoint = Text.format("{0}/_api/search/query?querytext='Path:{0}/* AND (contentclass:STS_Site OR contentclass:STS_Web)'&selectproperties='Path'&trimduplicates=false", domainUrl);
|
||||
|
||||
// Gets the available webs for the current domain with a search query
|
||||
this.spHttpClient.get(endpoint, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
response.json().then((data:any) => {
|
||||
try {
|
||||
let urls:string[] = [];
|
||||
let pathIndex = null;
|
||||
|
||||
for(let result of data.PrimaryQueryResult.RelevantResults.Table.Rows) {
|
||||
// Stores the index of the "Path" cell on the first loop in order to avoid finding the cell on every loop
|
||||
if(!pathIndex) {
|
||||
let pathCell = result.Cells.filter((cell) => { return cell.Key == "Path"; })[0];
|
||||
pathIndex = result.Cells.indexOf(pathCell);
|
||||
}
|
||||
urls.push(result.Cells[pathIndex].Value);
|
||||
}
|
||||
resolve(urls);
|
||||
}
|
||||
catch(error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
}
|
||||
else {
|
||||
reject(response.statusText);
|
||||
}
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { SPComponentLoader } from '@microsoft/sp-loader';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
export class TaxonomyService {
|
||||
|
||||
/***************************************************************************
|
||||
* The spHttpClient object used for performing REST calls to SharePoint
|
||||
***************************************************************************/
|
||||
private spHttpClient: SPHttpClient;
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Constructor
|
||||
* @param httpClient : The spHttpClient required to perform REST calls against SharePoint
|
||||
**************************************************************************************************/
|
||||
constructor(spHttpClient: SPHttpClient) {
|
||||
this.spHttpClient = spHttpClient;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Gets the taxonomy terms associated with the specified taxonomy field's termset
|
||||
* @param webUrl : The url of the web which contains the specified list
|
||||
* @param listTitle : The title of the list which contains the specified taxonomy field
|
||||
* @param fieldInternalName : The internal name of the taxonomy field on which to extract the termset
|
||||
**************************************************************************************************/
|
||||
public getSiteTaxonomyTermsByTermSet(webUrl: string, listTitle: string, fieldInternalName: string, lcid?: number): Promise<any> {
|
||||
return new Promise<any>((resolve,reject) => {
|
||||
|
||||
// Gets the termset ID associated with the list field
|
||||
this.getListFieldTermSetId(webUrl, listTitle, fieldInternalName).then((termsetId: string) => {
|
||||
|
||||
// Queries the Taxonomy Hidden list to retreive all terms with their wssIds
|
||||
let endpoint = Text.format("{0}/_api/web/lists/GetByTitle('TaxonomyHiddenList')/Items?$select=Term{1},ID&$filter=IdForTermSet eq '{2}'", webUrl, (lcid ? lcid : 1033), termsetId);
|
||||
this.spHttpClient.get(endpoint, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
resolve(response.json());
|
||||
}
|
||||
else {
|
||||
reject(response);
|
||||
}
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************
|
||||
* Gets the termset id out of the specified taxonomy field
|
||||
* @param webUrl : The url of the web which contains the specified list
|
||||
* @param listTitle : The title of the list which contains the sepcified field
|
||||
* @param fieldInternalName : The internal name of the field on which to extract its termset id
|
||||
**************************************************************************************************/
|
||||
public getListFieldTermSetId(webUrl: string, listTitle: string, fieldInternalName: string): Promise<string> {
|
||||
return new Promise<string>((resolve,reject) => {
|
||||
let endpoint = Text.format("{0}/_api/web/lists/GetByTitle('{1}')/Fields?$select=IsTermSetValid,TermSetId&$filter=InternalName eq '{2}'", webUrl, listTitle, fieldInternalName);
|
||||
this.spHttpClient.get(endpoint, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
|
||||
if(response.ok) {
|
||||
response.json().then((data:any) => {
|
||||
let fields:any[] = data.value;
|
||||
let fieldTermSetId = null;
|
||||
|
||||
if(fields.length > 0) {
|
||||
let field = fields[0];
|
||||
|
||||
if(field.IsTermSetValid && !isEmpty(field.TermSetId)) {
|
||||
fieldTermSetId = field.TermSetId;
|
||||
}
|
||||
}
|
||||
resolve(fieldTermSetId);
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
}
|
||||
else {
|
||||
reject(response);
|
||||
}
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Ensures SP.js and its dependencies in order to be able to do JSOM later on
|
||||
*************************************************************************************/
|
||||
private ensureJSOMDependencies(): Promise<{}> {
|
||||
if(window['SP']) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
else {
|
||||
return SPComponentLoader.loadScript('/_layouts/15/init.js', {
|
||||
globalExportsName: '$_global_init'
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
return SPComponentLoader.loadScript('/_layouts/15/MicrosoftAjax.js', {
|
||||
globalExportsName: 'Sys'
|
||||
});
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
return SPComponentLoader.loadScript('/_layouts/15/SP.Runtime.js', {
|
||||
globalExportsName: 'SP'
|
||||
});
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
return SPComponentLoader.loadScript('/_layouts/15/SP.js', {
|
||||
globalExportsName: 'SP'
|
||||
});
|
||||
})
|
||||
.then((): Promise<{}> => {
|
||||
return SPComponentLoader.loadScript('/_layouts/15/SP.Taxonomy.js', {
|
||||
globalExportsName: 'SP.Taxonomy'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneAsyncChecklistProps } from './IPropertyPaneAsyncChecklistProps';
|
||||
|
||||
export interface IPropertyPaneAsyncChecklistInternalProps extends IPropertyPaneAsyncChecklistProps, IPropertyPaneCustomFieldProps {
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { IChecklistItem } from './components/AsyncChecklist/IChecklistItem';
|
||||
import { IAsyncChecklistStrings } from './components/AsyncChecklist/IAsyncChecklistStrings';
|
||||
|
||||
export interface IPropertyPaneAsyncChecklistProps {
|
||||
loadItems: () => Promise<IChecklistItem[]>;
|
||||
onPropertyChange: (propertyPath: string, newCheckedKeys: string[]) => void;
|
||||
checkedItems: string[];
|
||||
disable?: boolean;
|
||||
strings: IAsyncChecklistStrings;
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { IPropertyPaneField, PropertyPaneFieldType } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneAsyncChecklistProps } from './IPropertyPaneAsyncChecklistProps';
|
||||
import { IPropertyPaneAsyncChecklistInternalProps } from './IPropertyPaneAsyncChecklistInternalProps';
|
||||
import { AsyncChecklist } from './components/AsyncChecklist/AsyncChecklist';
|
||||
import { IAsyncChecklistProps } from './components/AsyncChecklist/IAsyncChecklistProps';
|
||||
|
||||
|
||||
export class PropertyPaneAsyncChecklist implements IPropertyPaneField<IPropertyPaneAsyncChecklistProps> {
|
||||
|
||||
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||
public targetProperty: string;
|
||||
public properties: IPropertyPaneAsyncChecklistInternalProps;
|
||||
public loadedItems: boolean;
|
||||
private elem: HTMLElement;
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Property pane's contructor
|
||||
* @param targetProperty
|
||||
* @param properties
|
||||
*****************************************************************************************/
|
||||
constructor(targetProperty: string, properties: IPropertyPaneAsyncChecklistProps) {
|
||||
this.targetProperty = targetProperty;
|
||||
|
||||
this.properties = {
|
||||
loadItems: properties.loadItems,
|
||||
checkedItems: properties.checkedItems,
|
||||
onPropertyChange: properties.onPropertyChange,
|
||||
disable: properties.disable,
|
||||
strings: properties.strings,
|
||||
onRender: this.onRender.bind(this),
|
||||
key: targetProperty
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the AsyncChecklist property pane
|
||||
*****************************************************************************************/
|
||||
public render(): void {
|
||||
if (!this.elem) {
|
||||
return;
|
||||
}
|
||||
this.onRender(this.elem);
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the AsyncChecklist property pane
|
||||
*****************************************************************************************/
|
||||
private onRender(elem: HTMLElement): void {
|
||||
if (!this.elem) {
|
||||
this.elem = elem;
|
||||
}
|
||||
|
||||
const asyncChecklist: React.ReactElement<IAsyncChecklistProps> = React.createElement(AsyncChecklist, {
|
||||
loadItems: this.properties.loadItems,
|
||||
checkedItems: this.properties.checkedItems,
|
||||
onChange: this.onChange.bind(this),
|
||||
disable: this.properties.disable,
|
||||
strings: this.properties.strings,
|
||||
stateKey: new Date().toString()
|
||||
});
|
||||
|
||||
ReactDom.render(asyncChecklist, elem);
|
||||
this.loadedItems = true;
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Call the property pane's onPropertyChange when the QueryFilterPanel changes
|
||||
*****************************************************************************************/
|
||||
private onChange(checkedKeys: string[]): void {
|
||||
this.properties.onPropertyChange(this.targetProperty, checkedKeys);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
$lightgray: #f5f5f5;
|
||||
|
||||
.checklist {
|
||||
.checklistItems {
|
||||
background: $lightgray;
|
||||
padding: 12px;
|
||||
|
||||
.checklistPadding {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 280px;
|
||||
|
||||
.checklistItem {
|
||||
min-height: initial;
|
||||
margin-top: 9px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0px;
|
||||
|
||||
label {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import * as React from 'react';
|
||||
import { clone } from '@microsoft/sp-lodash-subset';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { Spinner, Label, Checkbox } from 'office-ui-fabric-react';
|
||||
import { IAsyncChecklistProps } from './IAsyncChecklistProps';
|
||||
import { IAsyncChecklistState } from './IAsyncChecklistState';
|
||||
import { IChecklistItem } from './IChecklistItem';
|
||||
import styles from './AsyncChecklist.module.scss';
|
||||
|
||||
export class AsyncChecklist extends React.Component<IAsyncChecklistProps, IAsyncChecklistState> {
|
||||
|
||||
/*************************************************************************************
|
||||
* Stores the checked items
|
||||
*************************************************************************************/
|
||||
private checkedItems: string[];
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Component's constructor
|
||||
*************************************************************************************/
|
||||
constructor(props: IAsyncChecklistProps, state: IAsyncChecklistState) {
|
||||
super(props);
|
||||
|
||||
this.state = { loading: true, items: [], error: null };
|
||||
this.checkedItems = this.getDefaultCheckedItems();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Gets the default checked items
|
||||
*************************************************************************************/
|
||||
private getDefaultCheckedItems() {
|
||||
return this.props.checkedItems ? clone(this.props.checkedItems) : new Array<string>();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When a checkbox changes within the checklist
|
||||
* @param ev : The React.FormEvent object which contains the element that has changed
|
||||
* @param checked : Whether the checkbox is not checked or not
|
||||
*************************************************************************************/
|
||||
private onCheckboxChange(ev?: React.FormEvent<HTMLInputElement>, checked?: boolean) {
|
||||
let checkboxKey = ev.currentTarget.attributes.getNamedItem('data').value;
|
||||
let itemIndex = this.checkedItems.indexOf(checkboxKey);
|
||||
|
||||
if(checked) {
|
||||
if(itemIndex == -1) {
|
||||
this.checkedItems.push(checkboxKey);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(itemIndex >= 0) {
|
||||
this.checkedItems.splice(itemIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.props.onChange) {
|
||||
this.props.onChange(this.checkedItems);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns whether the checkbox with the specified ID should be checked or not
|
||||
* @param checkboxId
|
||||
*************************************************************************************/
|
||||
private isCheckboxChecked(checkboxId: string) {
|
||||
return (this.checkedItems.filter((checkedItem) => { return checkedItem.toLowerCase().trim() == checkboxId.toLowerCase().trim(); }).length > 0);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Loads the checklist items asynchronously
|
||||
*************************************************************************************/
|
||||
private loadItems() {
|
||||
let _this_ = this;
|
||||
|
||||
_this_.checkedItems = this.getDefaultCheckedItems();
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
items: new Array<IChecklistItem>(),
|
||||
error: null
|
||||
});
|
||||
|
||||
this.props.loadItems().then((items: IChecklistItem[]) => {
|
||||
_this_.setState((prevState: IAsyncChecklistState, props: IAsyncChecklistProps): IAsyncChecklistState => {
|
||||
prevState.loading = false;
|
||||
prevState.items = items;
|
||||
return prevState;
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
_this_.setState((prevState: IAsyncChecklistState, props: IAsyncChecklistProps): IAsyncChecklistState => {
|
||||
prevState.loading = false;
|
||||
prevState.error = error;
|
||||
return prevState;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called once after initial rendering
|
||||
*************************************************************************************/
|
||||
public componentDidMount(): void {
|
||||
this.loadItems();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called immediately after updating occurs
|
||||
*************************************************************************************/
|
||||
public componentDidUpdate(prevProps: IAsyncChecklistProps, prevState: {}): void {
|
||||
if (this.props.disable !== prevProps.disable || this.props.stateKey !== prevProps.stateKey) {
|
||||
this.loadItems();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Renders the the QueryFilter component
|
||||
*************************************************************************************/
|
||||
public render() {
|
||||
const loading = this.state.loading ? <Spinner label={this.props.strings.loading} /> : <div />;
|
||||
const error = this.state.error != null ? <div className="ms-TextField-errorMessage ms-u-slideDownIn20">{ Text.format(this.props.strings.errorFormat, this.state.error) }</div> : <div />;
|
||||
|
||||
const checklistItems = this.state.items.map((item, index) => {
|
||||
return (
|
||||
<Checkbox data={ item.id }
|
||||
label={ item.label }
|
||||
defaultChecked={ this.isCheckboxChecked(item.id) }
|
||||
disabled={ this.props.disable }
|
||||
onChange={ this.onCheckboxChange.bind(this) }
|
||||
inputProps={ { data: item.id } }
|
||||
className={ styles.checklistItem }
|
||||
key={ index } />
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ styles.checklist }>
|
||||
|
||||
<Label>{ this.props.strings.label }</Label>
|
||||
|
||||
{ loading }
|
||||
|
||||
{ !this.state.loading &&
|
||||
<div className={ styles.checklistItems }>
|
||||
<div className={ styles.checklistPadding }>{ checklistItems }</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{ error }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { IChecklistItem } from './IChecklistItem';
|
||||
import { IAsyncChecklistStrings } from './IAsyncChecklistStrings';
|
||||
|
||||
export interface IAsyncChecklistProps {
|
||||
loadItems: () => Promise<IChecklistItem[]>;
|
||||
onChange?: (checkedKeys:string[]) => void;
|
||||
checkedItems: string[];
|
||||
disable?: boolean;
|
||||
strings: IAsyncChecklistStrings;
|
||||
stateKey?: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { IChecklistItem } from './IChecklistItem';
|
||||
|
||||
export interface IAsyncChecklistState {
|
||||
loading: boolean;
|
||||
items: IChecklistItem[];
|
||||
error: string;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface IAsyncChecklistStrings {
|
||||
label: string;
|
||||
loading: string;
|
||||
errorFormat: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface IChecklistItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneAsyncDropdownProps } from './IPropertyPaneAsyncDropdownProps';
|
||||
|
||||
export interface IPropertyPaneAsyncDropdownInternalProps extends IPropertyPaneAsyncDropdownProps, IPropertyPaneCustomFieldProps {
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { IDropdownOption } from 'office-ui-fabric-react';
|
||||
|
||||
export interface IPropertyPaneAsyncDropdownProps {
|
||||
label: string;
|
||||
loadingLabel: string;
|
||||
errorLabelFormat: string;
|
||||
loadOptions: () => Promise<IDropdownOption[]>;
|
||||
onPropertyChange: (propertyPath: string, newValue: any) => void;
|
||||
selectedKey?: string | number;
|
||||
disabled?: boolean;
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { IPropertyPaneField, PropertyPaneFieldType } from '@microsoft/sp-webpart-base';
|
||||
import { IDropdownOption } from 'office-ui-fabric-react';
|
||||
import { IPropertyPaneAsyncDropdownProps } from './IPropertyPaneAsyncDropdownProps';
|
||||
import { IPropertyPaneAsyncDropdownInternalProps } from './IPropertyPaneAsyncDropdownInternalProps';
|
||||
import { AsyncDropdown } from './components/AsyncDropdown/AsyncDropdown';
|
||||
import { IAsyncDropdownProps } from './components/AsyncDropdown/IAsyncDropdownProps';
|
||||
|
||||
|
||||
export class PropertyPaneAsyncDropdown implements IPropertyPaneField<IPropertyPaneAsyncDropdownProps> {
|
||||
|
||||
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||
public targetProperty: string;
|
||||
public properties: IPropertyPaneAsyncDropdownInternalProps;
|
||||
private elem: HTMLElement;
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Property pane's contructor
|
||||
* @param targetProperty
|
||||
* @param properties
|
||||
*****************************************************************************************/
|
||||
constructor(targetProperty: string, properties: IPropertyPaneAsyncDropdownProps) {
|
||||
this.targetProperty = targetProperty;
|
||||
this.properties = {
|
||||
label: properties.label,
|
||||
loadingLabel: properties.loadingLabel,
|
||||
errorLabelFormat: properties.errorLabelFormat,
|
||||
loadOptions: properties.loadOptions,
|
||||
onPropertyChange: properties.onPropertyChange,
|
||||
selectedKey: properties.selectedKey,
|
||||
disabled: properties.disabled,
|
||||
onRender: this.onRender.bind(this),
|
||||
key: targetProperty
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the AsyncDropdown property pane
|
||||
*****************************************************************************************/
|
||||
public render(): void {
|
||||
if (!this.elem) {
|
||||
return;
|
||||
}
|
||||
this.onRender(this.elem);
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the AsyncDropdown property pane
|
||||
*****************************************************************************************/
|
||||
private onRender(elem: HTMLElement): void {
|
||||
if (!this.elem) {
|
||||
this.elem = elem;
|
||||
}
|
||||
|
||||
const asyncDropDown: React.ReactElement<IAsyncDropdownProps> = React.createElement(AsyncDropdown, {
|
||||
label: this.properties.label,
|
||||
loadingLabel: this.properties.loadingLabel,
|
||||
errorLabelFormat: this.properties.errorLabelFormat,
|
||||
loadOptions: this.properties.loadOptions,
|
||||
onChanged: this.onChanged.bind(this),
|
||||
selectedKey: this.properties.selectedKey,
|
||||
disabled: this.properties.disabled,
|
||||
// required to allow the component to be re-rendered by calling this.render() externally
|
||||
stateKey: new Date().toString()
|
||||
});
|
||||
|
||||
ReactDom.render(asyncDropDown, elem);
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Call the property pane's onPropertyChange when the AsyncDropdown changes
|
||||
*****************************************************************************************/
|
||||
private onChanged(option: IDropdownOption, index?: number): void {
|
||||
this.properties.onPropertyChange(this.targetProperty, option.key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import * as React from 'react';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { Dropdown, IDropdownOption, Spinner } from 'office-ui-fabric-react';
|
||||
import { IAsyncDropdownProps } from './IAsyncDropdownProps';
|
||||
import { IAsyncDropdownState } from './IAsyncDropdownState';
|
||||
|
||||
export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDropdownState> {
|
||||
|
||||
/*************************************************************************************
|
||||
* Component's constructor
|
||||
* @param props
|
||||
* @param state
|
||||
*************************************************************************************/
|
||||
constructor(props: IAsyncDropdownProps, state: IAsyncDropdownState) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
processed: false,
|
||||
options: new Array<IDropdownOption>(),
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called once after initial rendering
|
||||
*************************************************************************************/
|
||||
public componentDidMount(): void {
|
||||
this.loadOptions();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called immediately after updating occurs
|
||||
*************************************************************************************/
|
||||
public componentDidUpdate(prevProps: IAsyncDropdownProps, prevState: IAsyncDropdownState): void {
|
||||
if (this.props.disabled !== prevProps.disabled || this.props.stateKey !== prevProps.stateKey) {
|
||||
this.loadOptions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Loads the dropdown options asynchronously
|
||||
*************************************************************************************/
|
||||
private loadOptions(): void {
|
||||
this.setState({
|
||||
processed: false,
|
||||
error: null,
|
||||
options: new Array<IDropdownOption>()
|
||||
});
|
||||
|
||||
this.props.loadOptions().then((options: IDropdownOption[]) => {
|
||||
this.setState({
|
||||
processed: true,
|
||||
error: null,
|
||||
options: options
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.setState((prevState: IAsyncDropdownState, props: IAsyncDropdownProps): IAsyncDropdownState => {
|
||||
prevState.processed = true;
|
||||
prevState.error = error;
|
||||
return prevState;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Renders the the AsyncDropdown component
|
||||
*************************************************************************************/
|
||||
public render() {
|
||||
|
||||
const loading = !this.state.processed ? <Spinner label={this.props.loadingLabel} /> : <div />;
|
||||
const error = this.state.error != null ? <div className="ms-TextField-errorMessage ms-u-slideDownIn20">{ Text.format(this.props.errorLabelFormat, this.state.error) }</div> : <div />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown label={this.props.label}
|
||||
isDisabled={this.props.disabled}
|
||||
onChanged={this.props.onChanged}
|
||||
selectedKey={this.props.selectedKey}
|
||||
options={this.state.options} />
|
||||
|
||||
{loading}
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { IDropdownOption } from 'office-ui-fabric-react';
|
||||
|
||||
export interface IAsyncDropdownProps {
|
||||
label: string;
|
||||
loadingLabel: string;
|
||||
errorLabelFormat: string;
|
||||
loadOptions: () => Promise<IDropdownOption[]>;
|
||||
onChanged?: (option: IDropdownOption, index?: number) => void;
|
||||
selectedKey?: string | number;
|
||||
disabled?: boolean;
|
||||
stateKey?: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { IDropdownOption } from 'office-ui-fabric-react';
|
||||
|
||||
export interface IAsyncDropdownState {
|
||||
processed: boolean;
|
||||
options: IDropdownOption[];
|
||||
error: string;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneQueryFilterPanelProps } from './IPropertyPaneQueryFilterPanelProps';
|
||||
|
||||
export interface IPropertyPaneQueryFilterPanelInternalProps extends IPropertyPaneQueryFilterPanelProps, IPropertyPaneCustomFieldProps {
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
import { IQueryFilter } from './components/QueryFilter/IQueryFilter';
|
||||
import { IQueryFilterField } from './components/QueryFilter/IQueryFilterField';
|
||||
import { IQueryFilterPanelStrings } from './components/QueryFilterPanel/IQueryFilterPanelStrings';
|
||||
|
||||
export interface IPropertyPaneQueryFilterPanelProps {
|
||||
filters: IQueryFilter[];
|
||||
loadFields: () => Promise<IQueryFilterField[]>;
|
||||
onLoadTaxonomyPickerSuggestions: (field: IQueryFilterField, filterText: string, currentTerms: ITag[]) => Promise<ITag[]>;
|
||||
onLoadPeoplePickerSuggestions: (filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) => Promise<IPersonaProps[]>;
|
||||
onPropertyChange: (propertyPath: string, newFilters: IQueryFilter[]) => void;
|
||||
trimEmptyFiltersOnChange?: boolean;
|
||||
disabled?: boolean;
|
||||
strings: IQueryFilterPanelStrings;
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { IPropertyPaneField, PropertyPaneFieldType } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneQueryFilterPanelProps } from './IPropertyPaneQueryFilterPanelProps';
|
||||
import { IPropertyPaneQueryFilterPanelInternalProps } from './IPropertyPaneQueryFilterPanelInternalProps';
|
||||
import { IQueryFilter } from './components/QueryFilter/IQueryFilter';
|
||||
import { QueryFilterPanel } from './components/QueryFilterPanel/QueryFilterPanel';
|
||||
import { IQueryFilterPanelProps } from './components/QueryFilterPanel/IQueryFilterPanelProps';
|
||||
|
||||
|
||||
export class PropertyPaneQueryFilterPanel implements IPropertyPaneField<IPropertyPaneQueryFilterPanelProps> {
|
||||
|
||||
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||
public targetProperty: string;
|
||||
public properties: IPropertyPaneQueryFilterPanelInternalProps;
|
||||
private elem: HTMLElement;
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Property pane's contructor
|
||||
* @param targetProperty
|
||||
* @param properties
|
||||
*****************************************************************************************/
|
||||
constructor(targetProperty: string, properties: IPropertyPaneQueryFilterPanelProps) {
|
||||
this.targetProperty = targetProperty;
|
||||
this.properties = {
|
||||
filters: properties.filters,
|
||||
loadFields: properties.loadFields,
|
||||
onLoadTaxonomyPickerSuggestions: properties.onLoadTaxonomyPickerSuggestions,
|
||||
onLoadPeoplePickerSuggestions: properties.onLoadPeoplePickerSuggestions,
|
||||
onPropertyChange: properties.onPropertyChange,
|
||||
trimEmptyFiltersOnChange: properties.trimEmptyFiltersOnChange,
|
||||
disabled: properties.disabled,
|
||||
strings: properties.strings,
|
||||
onRender: this.onRender.bind(this),
|
||||
key: targetProperty
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the QueryFilterPanel property pane
|
||||
*****************************************************************************************/
|
||||
public render(): void {
|
||||
if (!this.elem) {
|
||||
return;
|
||||
}
|
||||
this.onRender(this.elem);
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the QueryFilterPanel property pane
|
||||
*****************************************************************************************/
|
||||
private onRender(elem: HTMLElement): void {
|
||||
if (!this.elem) {
|
||||
this.elem = elem;
|
||||
}
|
||||
|
||||
const queryFilterpanel: React.ReactElement<IQueryFilterPanelProps> = React.createElement(QueryFilterPanel, {
|
||||
filters: this.properties.filters,
|
||||
loadFields: this.properties.loadFields,
|
||||
onLoadTaxonomyPickerSuggestions: this.properties.onLoadTaxonomyPickerSuggestions,
|
||||
onLoadPeoplePickerSuggestions: this.properties.onLoadPeoplePickerSuggestions,
|
||||
onChanged: this.onChanged.bind(this),
|
||||
trimEmptyFiltersOnChange: this.properties.trimEmptyFiltersOnChange,
|
||||
disabled: this.properties.disabled,
|
||||
strings: this.properties.strings,
|
||||
// required to allow the component to be re-rendered by calling this.render() externally
|
||||
stateKey: new Date().toString()
|
||||
});
|
||||
|
||||
ReactDom.render(queryFilterpanel, elem);
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Call the property pane's onPropertyChange when the QueryFilterPanel changes
|
||||
*****************************************************************************************/
|
||||
private onChanged(filters: IQueryFilter[]): void {
|
||||
this.properties.onPropertyChange(this.targetProperty, filters);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IQueryFilterField } from './IQueryFilterField';
|
||||
import { QueryFilterOperator } from './QueryFilterOperator';
|
||||
import { QueryFilterJoin } from './QueryFilterJoin';
|
||||
import { IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
|
||||
export interface IQueryFilter {
|
||||
index: number;
|
||||
field: IQueryFilterField;
|
||||
operator: QueryFilterOperator;
|
||||
value: string | IPersonaProps[] | ITag[] | Date;
|
||||
expression?: string;
|
||||
includeTime?: boolean;
|
||||
me?: boolean;
|
||||
join: QueryFilterJoin;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { QueryFilterFieldType } from './QueryFilterFieldType';
|
||||
|
||||
export interface IQueryFilterField {
|
||||
internalName: string;
|
||||
displayName: string;
|
||||
type: QueryFilterFieldType;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
import { IQueryFilter } from './IQueryFilter';
|
||||
import { IQueryFilterField } from './IQueryFilterField';
|
||||
import { IQueryFilterStrings } from './IQueryFilterStrings';
|
||||
|
||||
|
||||
export interface IQueryFilterProps {
|
||||
filter: IQueryFilter;
|
||||
fields: IQueryFilterField[];
|
||||
onLoadTaxonomyPickerSuggestions: (field: IQueryFilterField, filterText: string, currentTerms: ITag[]) => Promise<ITag[]>;
|
||||
onLoadPeoplePickerSuggestions: (filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) => Promise<IPersonaProps[]>;
|
||||
onChanged?: (filter: IQueryFilter) => void;
|
||||
disabled?: boolean;
|
||||
strings: IQueryFilterStrings;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IQueryFilter } from './IQueryFilter';
|
||||
|
||||
export interface IQueryFilterState {
|
||||
filter: IQueryFilter;
|
||||
pickersKey: number;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { IDatePickerStrings } from 'office-ui-fabric-react';
|
||||
|
||||
export interface IQueryFilterStrings {
|
||||
fieldLabel: string;
|
||||
fieldSelectLabel: string;
|
||||
operatorLabel: string;
|
||||
operatorEqualLabel: string;
|
||||
operatorNotEqualLabel: string;
|
||||
operatorGreaterLabel: string;
|
||||
operatorGreaterEqualLabel: string;
|
||||
operatorLessLabel: string;
|
||||
operatorLessEqualLabel: string;
|
||||
operatorContainsLabel: string;
|
||||
operatorBeginsWithLabel: string;
|
||||
operatorContainsAnyLabel: string;
|
||||
operatorContainsAllLabel: string;
|
||||
operatorIsNullLabel: string;
|
||||
operatorIsNotNullLabel: string;
|
||||
valueLabel: string;
|
||||
andLabel: string;
|
||||
orLabel: string;
|
||||
peoplePickerSuggestionHeader: string;
|
||||
peoplePickerNoResults: string;
|
||||
peoplePickerLoading: string;
|
||||
peoplePickerMe: string;
|
||||
taxonomyPickerSuggestionHeader: string;
|
||||
taxonomyPickerNoResults: string;
|
||||
taxonomyPickerLoading: string;
|
||||
datePickerStrings: IDatePickerStrings;
|
||||
datePickerLocale: string;
|
||||
datePickerFormat: string;
|
||||
datePickerExpressionError: string;
|
||||
datePickerDatePlaceholder: string;
|
||||
datePickerExpressionPlaceholder: string;
|
||||
datePickerIncludeTime: string;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
$lightgray: #f5f5f5;
|
||||
|
||||
.queryFilter {
|
||||
background-color: $lightgray;
|
||||
|
||||
&.disabled {
|
||||
background-color: transparent;
|
||||
border: 1px solid $lightgray;
|
||||
}
|
||||
|
||||
.paddingContainer {
|
||||
padding: 12px 15px 3px 15px;
|
||||
|
||||
div[class~=ms-ChoiceFieldGroup] {
|
||||
text-align: center;
|
||||
|
||||
div[class~=ms-ChoiceField] {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
div[class~="ms-BasePicker-text"] {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.peoplePicker {
|
||||
&.disabled {
|
||||
div[class~="ms-PickerPersona-container"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span[class~="ms-TagItem-text"] {
|
||||
max-width: 201px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,421 @@
|
|||
import * as React from 'react';
|
||||
import * as moment from 'moment';
|
||||
import { cloneDeep, isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { Dropdown, IDropdownOption, TextField, ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react';
|
||||
import { NormalPeoplePicker, IPersonaProps, IBasePickerSuggestionsProps, Label } from 'office-ui-fabric-react';
|
||||
import { TagPicker, ITag } from 'office-ui-fabric-react';
|
||||
import { DatePicker, Checkbox } from 'office-ui-fabric-react';
|
||||
import { IQueryFilter } from './IQueryFilter';
|
||||
import { QueryFilterOperator } from './QueryFilterOperator';
|
||||
import { QueryFilterJoin } from './QueryFilterJoin';
|
||||
import { QueryFilterFieldType } from './QueryFilterFieldType';
|
||||
import { IQueryFilterProps } from './IQueryFilterProps';
|
||||
import { IQueryFilterState } from './IQueryFilterState';
|
||||
import styles from './QueryFilter.module.scss';
|
||||
|
||||
export class QueryFilter extends React.Component<IQueryFilterProps, IQueryFilterState> {
|
||||
|
||||
/*************************************************************************************
|
||||
* Stores the IQueryFilter config of the current filter
|
||||
*************************************************************************************/
|
||||
private filter:IQueryFilter;
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Component's constructor
|
||||
* @param props
|
||||
* @param state
|
||||
*************************************************************************************/
|
||||
constructor(props: IQueryFilterProps, state: IQueryFilterState) {
|
||||
super(props);
|
||||
|
||||
moment.locale(this.props.strings.datePickerLocale);
|
||||
|
||||
this.state = {
|
||||
filter: (this.props.filter ? cloneDeep(this.props.filter) : { index: 0, field: null, operator: QueryFilterOperator.Eq, value: '', join: QueryFilterJoin.Or }),
|
||||
pickersKey: Math.random()
|
||||
};
|
||||
|
||||
this.onAnyChange = this.onAnyChange.bind(this);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the field Dropdown changes
|
||||
*************************************************************************************/
|
||||
private onFieldDropdownChange(option: IDropdownOption, index?: number) {
|
||||
let field = this.props.fields.filter((f) => { return f.internalName == option.key; });
|
||||
this.state.filter.field = field != null && field.length > 0 ? field[0] : null;
|
||||
this.state.filter.operator = (this.state.filter.field && (this.state.filter.field.type == QueryFilterFieldType.User || this.state.filter.field.type == QueryFilterFieldType.Taxonomy) ? QueryFilterOperator.ContainsAny : QueryFilterOperator.Eq);
|
||||
this.state.filter.value = null;
|
||||
this.state.filter.me = false;
|
||||
this.state.filter.includeTime = false;
|
||||
this.state.filter.expression = null;
|
||||
this.setState({ filter: this.state.filter, pickersKey: Math.random() });
|
||||
this.onAnyChange();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the operator Dropdown changes
|
||||
*************************************************************************************/
|
||||
private onOperatorDropdownChange(option: IDropdownOption, index?: number) {
|
||||
this.state.filter.operator = QueryFilterOperator[option.key];
|
||||
this.setState({ filter: this.state.filter, pickersKey: this.state.pickersKey });
|
||||
this.onAnyChange();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the TextField value changes
|
||||
*************************************************************************************/
|
||||
private onValueTextFieldChange(newValue: string): string {
|
||||
if(this.state.filter.value != newValue) {
|
||||
this.state.filter.value = newValue;
|
||||
this.onAnyChange();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the people picker value changes
|
||||
*************************************************************************************/
|
||||
private onPeoplePickerResolve(items: IPersonaProps[]) {
|
||||
this.state.filter.value = items;
|
||||
this.onAnyChange();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the "Me" checkbox changes
|
||||
* @param ev : The React.FormEvent object which contains the element that has changed
|
||||
* @param checked : Whether the checkbox is not checked or not
|
||||
*************************************************************************************/
|
||||
private onPeoplePickerCheckboxChange(ev?: React.FormEvent<HTMLInputElement>, checked?: boolean) {
|
||||
this.state.filter.me = checked;
|
||||
this.setState({ filter: this.state.filter, pickersKey: this.state.pickersKey });
|
||||
this.onAnyChange();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the NormalPeoplePicker value changes
|
||||
*************************************************************************************/
|
||||
private onTaxonomyPickerResolve(items: ITag[]) {
|
||||
this.state.filter.value = items;
|
||||
this.onAnyChange();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the date picker value changes
|
||||
*************************************************************************************/
|
||||
private onDatePickerChange(date: Date) {
|
||||
this.state.filter.value = date;
|
||||
this.state.filter.expression = '';
|
||||
this.setState({ filter: this.state.filter, pickersKey: this.state.pickersKey });
|
||||
this.onAnyChange();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the date expression text field value changes
|
||||
*************************************************************************************/
|
||||
private onDateExpressionChange(newValue: string): string {
|
||||
|
||||
// Validates the picker
|
||||
let regex = new RegExp(/^\[Today\](\s{0,}[\+-]\s{0,}\[{0,1}\d{1,4}\]{0,1}){0,1}$/);
|
||||
let isValid = regex.test(newValue) || isEmpty(newValue);
|
||||
let errorMsg = isValid ? '' : this.props.strings.datePickerExpressionError;
|
||||
|
||||
if(isValid) {
|
||||
// If the change is NOT triggered by the date picker change
|
||||
if(!(isEmpty(newValue) && this.state.filter.value != null)) {
|
||||
this.state.filter.value = null;
|
||||
this.state.filter.expression = newValue;
|
||||
this.setState({ filter: this.state.filter, pickersKey: this.state.pickersKey });
|
||||
this.onAnyChange();
|
||||
}
|
||||
}
|
||||
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the include time checkbox changes
|
||||
* @param ev : The React.FormEvent object which contains the element that has changed
|
||||
* @param checked : Whether the checkbox is not checked or not
|
||||
*************************************************************************************/
|
||||
private onDateIncludeTimeChange(ev?: React.FormEvent<HTMLInputElement>, checked?: boolean) {
|
||||
this.state.filter.includeTime = checked;
|
||||
this.onAnyChange();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the join ChoiceGroup changes
|
||||
*************************************************************************************/
|
||||
private onJoinChoiceChange(ev?: React.FormEvent<HTMLInputElement>, option?: IChoiceGroupOption) {
|
||||
if(option) {
|
||||
this.state.filter.join = QueryFilterJoin[option.key];
|
||||
this.onAnyChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Call the parent onChanged with the updated IQueryFilter object
|
||||
*************************************************************************************/
|
||||
private onAnyChange() {
|
||||
if(this.props.onChanged) {
|
||||
this.props.onChanged(this.state.filter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns the options for the field Dropdown component
|
||||
*************************************************************************************/
|
||||
private getFieldDropdownOptions(): IDropdownOption[] {
|
||||
let options:IDropdownOption[] = [
|
||||
{ key: "", text: this.props.strings.fieldSelectLabel }
|
||||
];
|
||||
|
||||
for(let field of this.props.fields) {
|
||||
let option:IDropdownOption = { key: field.internalName, text: Text.format("{0} \{\{{1}\}\}", field.displayName, field.internalName) };
|
||||
options.push(option);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns the options for the operator Dropdown component
|
||||
*************************************************************************************/
|
||||
private getOperatorDropdownOptions(): IDropdownOption[] {
|
||||
let fieldType = this.state.filter.field ? this.state.filter.field.type : QueryFilterFieldType.Text;
|
||||
let options:IDropdownOption[];
|
||||
|
||||
// Operators for User and Taxonomy field types
|
||||
if(fieldType == QueryFilterFieldType.User || fieldType == QueryFilterFieldType.Taxonomy) {
|
||||
options = [
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.ContainsAny], text: this.props.strings.operatorContainsAnyLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.ContainsAll], text: this.props.strings.operatorContainsAllLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.IsNull], text: this.props.strings.operatorIsNullLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.IsNotNull], text: this.props.strings.operatorIsNotNullLabel }
|
||||
];
|
||||
}
|
||||
|
||||
// Operators for Text, Number, Datetime and Lookup field types
|
||||
else {
|
||||
options = [
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.Eq], text: this.props.strings.operatorEqualLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.Neq], text: this.props.strings.operatorNotEqualLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.Gt], text: this.props.strings.operatorGreaterLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.Lt], text: this.props.strings.operatorLessLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.Geq], text: this.props.strings.operatorGreaterEqualLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.Leq], text: this.props.strings.operatorLessEqualLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.IsNull], text: this.props.strings.operatorIsNullLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.IsNotNull], text: this.props.strings.operatorIsNotNullLabel }
|
||||
];
|
||||
|
||||
// Specific operators for text field type
|
||||
if(fieldType == QueryFilterFieldType.Text) {
|
||||
options = options.concat([
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.BeginsWith], text: this.props.strings.operatorBeginsWithLabel },
|
||||
{ key: QueryFilterOperator[QueryFilterOperator.Contains], text: this.props.strings.operatorContainsLabel }
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns the options for the operator Dropdown component
|
||||
*************************************************************************************/
|
||||
private getJoinGroupOptions(): IChoiceGroupOption[] {
|
||||
let options:IChoiceGroupOption[] = [
|
||||
{ key: QueryFilterJoin[QueryFilterJoin.And], text: this.props.strings.andLabel, checked: (this.state.filter.join == QueryFilterJoin.And) },
|
||||
{ key: QueryFilterJoin[QueryFilterJoin.Or], text: this.props.strings.orLabel, checked: (this.state.filter.join == QueryFilterJoin.Or) }
|
||||
];
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns the user suggestions based on the specified user-entered filter
|
||||
*************************************************************************************/
|
||||
private onLoadPeoplePickerSuggestions(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) {
|
||||
if(isEmpty(filterText)) {
|
||||
return [];
|
||||
}
|
||||
return this.props.onLoadPeoplePickerSuggestions(filterText, currentPersonas, limitResults);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns the tag suggestions based on the specified user-entered filter
|
||||
*************************************************************************************/
|
||||
private onLoadTagPickerSuggestions(filterText: string, currentTerms: ITag[]) {
|
||||
if(isEmpty(filterText)) {
|
||||
return [];
|
||||
}
|
||||
return this.props.onLoadTaxonomyPickerSuggestions(this.state.filter.field, filterText, currentTerms);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Converts the specified filter value into a Date object if valid, otherwise null
|
||||
* @param dateValue : The filter value that must be transformed into a Date object
|
||||
*************************************************************************************/
|
||||
private getDatePickerValue(dateValue: string | Date | IPersonaProps[] | ITag[]): Date {
|
||||
if(dateValue instanceof Date) {
|
||||
return dateValue;
|
||||
}
|
||||
else if(typeof(dateValue) === 'string') {
|
||||
let date = moment(dateValue, moment.ISO_8601, true);
|
||||
|
||||
if(date.isValid()) {
|
||||
return date.toDate();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Converts the date resolved by the DatePicker into a formatted string
|
||||
* @param date : The date resolved by the DatePicker
|
||||
*************************************************************************************/
|
||||
private onDatePickerFormat(date: Date): string {
|
||||
return moment(date).format(this.props.strings.datePickerFormat);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Converts the string manually entered by the user in the people picker to a Date
|
||||
* @param dateStr : The string that must be parsed to a Date object
|
||||
*************************************************************************************/
|
||||
private onDatePickerParse(dateStr: string) : Date {
|
||||
let date = moment(dateStr, this.props.strings.datePickerFormat, true);
|
||||
return date.toDate();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Renders the the QueryFilter component
|
||||
*************************************************************************************/
|
||||
public render() {
|
||||
const filterFieldKey = this.state.filter.field != null ? this.state.filter.field.internalName : "";
|
||||
const datePickerValue = this.getDatePickerValue(this.state.filter.value);
|
||||
const hideValueSection = this.state.filter.operator == QueryFilterOperator.IsNull || this.state.filter.operator == QueryFilterOperator.IsNotNull;
|
||||
|
||||
const showTextField = (!this.state.filter.field || (this.state.filter.field.type == QueryFilterFieldType.Text || this.state.filter.field.type == QueryFilterFieldType.Number || this.state.filter.field.type == QueryFilterFieldType.Lookup)) && !hideValueSection;
|
||||
const showPeoplePicker = this.state.filter.field && this.state.filter.field.type == QueryFilterFieldType.User && !hideValueSection;
|
||||
const showTaxonomyPicker = this.state.filter.field && this.state.filter.field.type == QueryFilterFieldType.Taxonomy && !hideValueSection;
|
||||
const showDatePicker = this.state.filter.field && this.state.filter.field.type == QueryFilterFieldType.Datetime && !hideValueSection;
|
||||
|
||||
const taxonomyPickerSuggestionProps: IBasePickerSuggestionsProps = {
|
||||
suggestionsHeaderText: this.props.strings.taxonomyPickerSuggestionHeader,
|
||||
noResultsFoundText: this.props.strings.taxonomyPickerNoResults,
|
||||
loadingText: this.props.strings.taxonomyPickerLoading
|
||||
};
|
||||
|
||||
const peoplePickerSuggestionProps: IBasePickerSuggestionsProps = {
|
||||
suggestionsHeaderText: this.props.strings.peoplePickerSuggestionHeader,
|
||||
noResultsFoundText: this.props.strings.peoplePickerNoResults,
|
||||
loadingText: this.props.strings.peoplePickerLoading
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.queryFilter + ' ' + (this.props.disabled ? styles.disabled : '')}>
|
||||
<div className={styles.paddingContainer}>
|
||||
<Dropdown label={this.props.strings.fieldLabel}
|
||||
disabled={this.props.disabled}
|
||||
onChanged={this.onFieldDropdownChange.bind(this)}
|
||||
selectedKey={filterFieldKey}
|
||||
options={this.getFieldDropdownOptions()} />
|
||||
|
||||
<Dropdown label={this.props.strings.operatorLabel}
|
||||
disabled={this.props.disabled}
|
||||
onChanged={this.onOperatorDropdownChange.bind(this)}
|
||||
selectedKey={QueryFilterOperator[this.state.filter.operator]}
|
||||
options={this.getOperatorDropdownOptions()} />
|
||||
|
||||
{ showTextField &&
|
||||
<TextField label={this.props.strings.valueLabel}
|
||||
disabled={this.props.disabled}
|
||||
onGetErrorMessage={ this.onValueTextFieldChange.bind(this) }
|
||||
deferredValidationTime={500}
|
||||
value={ this.state.filter.value != null ? this.state.filter.value as string : '' } />
|
||||
}
|
||||
|
||||
{ showPeoplePicker &&
|
||||
<div>
|
||||
<Label>{ this.props.strings.valueLabel }</Label>
|
||||
<NormalPeoplePicker
|
||||
onResolveSuggestions={ this.onLoadPeoplePickerSuggestions.bind(this) }
|
||||
onChange={ this.onPeoplePickerResolve.bind(this) }
|
||||
defaultSelectedItems= { this.state.filter.value as IPersonaProps[] }
|
||||
getTextFromItem={ (user: IPersonaProps) => user.primaryText }
|
||||
pickerSuggestionsProps={ peoplePickerSuggestionProps }
|
||||
className={ styles.peoplePicker + (this.state.filter.me ? ' ' + styles.disabled : '') }
|
||||
inputProps={{ disabled: this.state.filter.me }}
|
||||
key={ "peoplePicker" + this.state.pickersKey } />
|
||||
<Checkbox
|
||||
label={ this.props.strings.peoplePickerMe }
|
||||
onChange={ this.onPeoplePickerCheckboxChange.bind(this) }
|
||||
checked={ this.state.filter.me } />
|
||||
</div>
|
||||
}
|
||||
|
||||
{ showTaxonomyPicker &&
|
||||
<div>
|
||||
<Label>{ this.props.strings.valueLabel }</Label>
|
||||
<TagPicker
|
||||
onResolveSuggestions={ this.onLoadTagPickerSuggestions.bind(this) }
|
||||
onChange={ this.onTaxonomyPickerResolve.bind(this) }
|
||||
defaultSelectedItems= { this.state.filter.value as ITag[] }
|
||||
getTextFromItem={ (term: ITag) => term.name }
|
||||
pickerSuggestionsProps={ taxonomyPickerSuggestionProps }
|
||||
key={ "taxonomyPicker" + this.state.pickersKey } />
|
||||
</div>
|
||||
}
|
||||
|
||||
{ showDatePicker &&
|
||||
<div>
|
||||
<DatePicker
|
||||
label={ this.props.strings.valueLabel }
|
||||
placeholder={ this.props.strings.datePickerDatePlaceholder }
|
||||
allowTextInput={ true }
|
||||
value={ datePickerValue }
|
||||
formatDate={ this.onDatePickerFormat.bind(this) }
|
||||
parseDateFromString={ this.onDatePickerParse.bind(this) }
|
||||
onSelectDate={ this.onDatePickerChange.bind(this) }
|
||||
strings={ this.props.strings.datePickerStrings } />
|
||||
<TextField
|
||||
placeholder={ this.props.strings.datePickerExpressionPlaceholder }
|
||||
onGetErrorMessage={ this.onDateExpressionChange.bind(this) }
|
||||
deferredValidationTime={ 500 }
|
||||
value={ this.state.filter.expression || '' } />
|
||||
<Checkbox
|
||||
label={ this.props.strings.datePickerIncludeTime }
|
||||
onChange={ this.onDateIncludeTimeChange.bind(this) }
|
||||
checked={ this.state.filter.includeTime } />
|
||||
</div>
|
||||
}
|
||||
|
||||
<ChoiceGroup options={this.getJoinGroupOptions()}
|
||||
onChange={this.onJoinChoiceChange.bind(this)}
|
||||
disabled={this.props.disabled} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export enum QueryFilterFieldType {
|
||||
Text = 1,
|
||||
Number= 2,
|
||||
Datetime = 3,
|
||||
User = 4,
|
||||
Lookup = 5,
|
||||
Taxonomy = 6,
|
||||
Url = 7
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export enum QueryFilterJoin {
|
||||
And = 1,
|
||||
Or = 2
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
export enum QueryFilterOperator {
|
||||
Eq = 1,
|
||||
Neq = 2,
|
||||
Gt = 3,
|
||||
Lt = 4,
|
||||
Geq = 5,
|
||||
Leq = 6,
|
||||
Contains = 7,
|
||||
BeginsWith = 8,
|
||||
ContainsAll = 9,
|
||||
ContainsAny = 10,
|
||||
IsNull = 11,
|
||||
IsNotNull = 12
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
import { IQueryFilter } from '../QueryFilter/IQueryFilter';
|
||||
import { IQueryFilterField } from '../QueryFilter/IQueryFilterField';
|
||||
import { IQueryFilterPanelStrings } from './IQueryFilterPanelStrings';
|
||||
|
||||
export interface IQueryFilterPanelProps {
|
||||
filters: IQueryFilter[];
|
||||
loadFields: () => Promise<IQueryFilterField[]>;
|
||||
onLoadTaxonomyPickerSuggestions: (field: IQueryFilterField, filterText: string, currentTerms: ITag[]) => Promise<ITag[]>;
|
||||
onLoadPeoplePickerSuggestions: (filterText: string, currentPersonas: IPersonaProps[], limitResults?: number) => Promise<IPersonaProps[]>;
|
||||
onChanged?: (filters: IQueryFilter[]) => void;
|
||||
disabled?: boolean;
|
||||
trimEmptyFiltersOnChange?: boolean;
|
||||
strings: IQueryFilterPanelStrings;
|
||||
stateKey?: string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { IQueryFilter } from '../QueryFilter/IQueryFilter';
|
||||
import { IQueryFilterField } from '../QueryFilter/IQueryFilterField';
|
||||
|
||||
export interface IQueryFilterPanelState {
|
||||
loading: boolean;
|
||||
fields: IQueryFilterField[];
|
||||
filters: IQueryFilter[];
|
||||
error: string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { IQueryFilterStrings } from '../QueryFilter/IQueryFilterStrings';
|
||||
|
||||
export interface IQueryFilterPanelStrings {
|
||||
filtersLabel: string;
|
||||
loadingFieldsLabel: string;
|
||||
loadingFieldsErrorLabel: string;
|
||||
addFilterLabel: string;
|
||||
queryFilterStrings: IQueryFilterStrings;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
.queryFilterPanel {
|
||||
|
||||
.queryFilterPanelItems {
|
||||
.queryFilterPanelItem {
|
||||
margin-top: 15px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>button {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
import * as React from 'react';
|
||||
import { cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import { Spinner, Button, ButtonType, Label } from 'office-ui-fabric-react';
|
||||
import { QueryFilter } from '../QueryFilter/QueryFilter';
|
||||
import { IQueryFilter } from '../QueryFilter/IQueryFilter';
|
||||
import { QueryFilterOperator } from '../QueryFilter/QueryFilterOperator';
|
||||
import { QueryFilterJoin } from '../QueryFilter/QueryFilterJoin';
|
||||
import { IQueryFilterField } from '../QueryFilter/IQueryFilterField';
|
||||
import { IQueryFilterPanelProps } from './IQueryFilterPanelProps';
|
||||
import { IQueryFilterPanelState } from './IQueryFilterPanelState';
|
||||
import styles from './QueryFilterPanel.module.scss';
|
||||
|
||||
|
||||
export class QueryFilterPanel extends React.Component<IQueryFilterPanelProps, IQueryFilterPanelState> {
|
||||
|
||||
/*************************************************************************************
|
||||
* Component's constructor
|
||||
* @param props
|
||||
* @param state
|
||||
*************************************************************************************/
|
||||
constructor(props: IQueryFilterPanelProps, state: IQueryFilterPanelState) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
fields: [],
|
||||
filters: this.getDefaultFilters(),
|
||||
error: null
|
||||
};
|
||||
|
||||
this.getDefaultFilters = this.getDefaultFilters.bind(this);
|
||||
this.loadFields = this.loadFields.bind(this);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns a default array with an empty filter
|
||||
*************************************************************************************/
|
||||
private getDefaultFilters():IQueryFilter[] {
|
||||
if(this.props.filters != null && this.props.filters.length > 0) {
|
||||
return this.sortFiltersByIndex(this.props.filters);
|
||||
}
|
||||
|
||||
let defaultFilters:IQueryFilter[] = [
|
||||
{ index: 0, field: null, operator: QueryFilterOperator.Eq, join: QueryFilterJoin.Or, value: '' }
|
||||
];
|
||||
return defaultFilters;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called once after initial rendering
|
||||
*************************************************************************************/
|
||||
public componentDidMount(): void {
|
||||
this.loadFields();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called immediately after updating occurs
|
||||
*************************************************************************************/
|
||||
public componentDidUpdate(prevProps: IQueryFilterPanelProps, prevState: IQueryFilterPanelState): void {
|
||||
if (this.props.disabled !== prevProps.disabled || this.props.stateKey !== prevProps.stateKey) {
|
||||
this.loadFields();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Loads the available fields asynchronously
|
||||
*************************************************************************************/
|
||||
private loadFields(): void {
|
||||
|
||||
this.setState((prevState: IQueryFilterPanelState, props: IQueryFilterPanelProps): IQueryFilterPanelState => {
|
||||
prevState.loading = true;
|
||||
prevState.error = null;
|
||||
return prevState;
|
||||
});
|
||||
|
||||
this.props.loadFields().then((fields: IQueryFilterField[]) => {
|
||||
this.setState((prevState: IQueryFilterPanelState, props: IQueryFilterPanelProps): IQueryFilterPanelState => {
|
||||
prevState.loading = false;
|
||||
prevState.fields = fields;
|
||||
prevState.filters = this.getDefaultFilters();
|
||||
return prevState;
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.setState((prevState: IQueryFilterPanelState, props: IQueryFilterPanelProps): IQueryFilterPanelState => {
|
||||
prevState.loading = false;
|
||||
prevState.error = error;
|
||||
return prevState;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When one of the filter changes
|
||||
*************************************************************************************/
|
||||
private onFilterChanged(filter:IQueryFilter): void {
|
||||
// Makes sure the parent is not notified for no reason if the modified filter was (and still is) considered empty
|
||||
let isWorthNotifyingParent = true;
|
||||
let oldFilter = this.state.filters.filter((i) => { return i.index == filter.index; })[0];
|
||||
let oldFilterIndex = this.state.filters.indexOf(oldFilter);
|
||||
|
||||
if(this.props.trimEmptyFiltersOnChange && this.isFilterEmpty(oldFilter) && this.isFilterEmpty(filter)) {
|
||||
isWorthNotifyingParent = false;
|
||||
}
|
||||
|
||||
// Updates the modified filter in the state
|
||||
this.state.filters[oldFilterIndex] = cloneDeep(filter);
|
||||
this.setState((prevState: IQueryFilterPanelState, props: IQueryFilterPanelProps): IQueryFilterPanelState => {
|
||||
prevState.filters = this.state.filters;
|
||||
return prevState;
|
||||
});
|
||||
|
||||
// Notifies the parent with the updated filters
|
||||
if(isWorthNotifyingParent) {
|
||||
let filters:IQueryFilter[] = this.props.trimEmptyFiltersOnChange ? this.state.filters.filter((f) => { return !this.isFilterEmpty(f); }) : this.state.filters;
|
||||
this.props.onChanged(filters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns whether the specified filter is empty or not
|
||||
* @param filter : The filter that needs to be checked
|
||||
*************************************************************************************/
|
||||
private isFilterEmpty(filter:IQueryFilter) {
|
||||
let isFilterEmpty = false;
|
||||
|
||||
// If the filter has no field
|
||||
if(filter.field == null) {
|
||||
isFilterEmpty = true;
|
||||
}
|
||||
|
||||
// If the filter has a null or empty value
|
||||
if(filter.value == null || isEmpty(filter.value.toString())) {
|
||||
|
||||
// And has no date time expression
|
||||
if(isEmpty(filter.expression)) {
|
||||
|
||||
// And isn't a [Me] switch
|
||||
if(!filter.me) {
|
||||
|
||||
// And isn't a <IsNull /> or <IsNotNull /> operator
|
||||
if(filter.operator != QueryFilterOperator.IsNull && filter.operator != QueryFilterOperator.IsNotNull) {
|
||||
isFilterEmpty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return isFilterEmpty;
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* When the 'Add filter' button is clicked
|
||||
*************************************************************************************/
|
||||
private onAddFilterClick(): void {
|
||||
// Updates the state with an all fresh new filter
|
||||
let nextAvailableFilterIndex = this.state.filters[this.state.filters.length-1].index + 1;
|
||||
let newFilter:IQueryFilter = { index: nextAvailableFilterIndex, field: null, operator: QueryFilterOperator.Eq, join: QueryFilterJoin.Or, value: '' };
|
||||
this.state.filters.push(newFilter);
|
||||
|
||||
this.setState((prevState: IQueryFilterPanelState, props: IQueryFilterPanelProps): IQueryFilterPanelState => {
|
||||
prevState.filters = this.state.filters;
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private sortFiltersByIndex(filters:IQueryFilter[]): IQueryFilter[] {
|
||||
return filters.sort((a, b) => {
|
||||
if(a.index > b.index) { return 1; } else { return 0; }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Renders the the QueryFilter component
|
||||
*************************************************************************************/
|
||||
public render() {
|
||||
const loading = this.state.loading ? <Spinner label={this.props.strings.loadingFieldsLabel} /> : <div />;
|
||||
const error = this.state.error != null ? <div className="ms-TextField-errorMessage ms-u-slideDownIn20">{ Text.format(this.props.strings.loadingFieldsErrorLabel, this.state.error) }</div> : <div />;
|
||||
|
||||
const filters = this.state.filters.map((filter, index) =>
|
||||
<div className={styles.queryFilterPanelItem} key={index}>
|
||||
<QueryFilter fields={this.state.fields}
|
||||
filter={filter}
|
||||
disabled={this.props.disabled}
|
||||
onLoadTaxonomyPickerSuggestions={this.props.onLoadTaxonomyPickerSuggestions}
|
||||
onLoadPeoplePickerSuggestions={this.props.onLoadPeoplePickerSuggestions}
|
||||
onChanged={this.onFilterChanged.bind(this)}
|
||||
strings={this.props.strings.queryFilterStrings}
|
||||
key={index} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.queryFilterPanel}>
|
||||
<Label>{this.props.strings.filtersLabel}</Label>
|
||||
|
||||
{loading}
|
||||
|
||||
{ !this.state.loading &&
|
||||
<div className={styles.queryFilterPanelItems}>{filters}</div>
|
||||
}
|
||||
|
||||
{ !this.state.loading &&
|
||||
<Button buttonType={ButtonType.primary} onClick={this.onAddFilterClick.bind(this)} disabled={this.props.disabled} icon='Add'>{this.props.strings.addFilterLabel}</Button>
|
||||
}
|
||||
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneTextDialogProps } from './IPropertyPaneTextDialogProps';
|
||||
|
||||
export interface IPropertyPaneTextDialogInternalProps extends IPropertyPaneTextDialogProps, IPropertyPaneCustomFieldProps {
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { ITextDialogStrings } from "./components/TextDialog/ITextDialogStrings";
|
||||
|
||||
export interface IPropertyPaneTextDialogProps {
|
||||
dialogTextFieldValue?: string;
|
||||
onPropertyChange: (propertyPath: string, text: string) => void;
|
||||
disabled?: boolean;
|
||||
strings: ITextDialogStrings;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { IPropertyPaneField, PropertyPaneFieldType } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneTextDialogProps } from './IPropertyPaneTextDialogProps';
|
||||
import { IPropertyPaneTextDialogInternalProps } from './IPropertyPaneTextDialogInternalProps';
|
||||
import { TextDialog } from './components/TextDialog/TextDialog';
|
||||
import { ITextDialogProps } from './components/TextDialog/ITextDialogProps';
|
||||
|
||||
|
||||
export class PropertyPaneTextDialog implements IPropertyPaneField<IPropertyPaneTextDialogProps> {
|
||||
|
||||
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||
public targetProperty: string;
|
||||
public properties: IPropertyPaneTextDialogInternalProps;
|
||||
private elem: HTMLElement;
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Property pane's contructor
|
||||
* @param targetProperty
|
||||
* @param properties
|
||||
*****************************************************************************************/
|
||||
constructor(targetProperty: string, properties: IPropertyPaneTextDialogProps) {
|
||||
this.targetProperty = targetProperty;
|
||||
this.properties = {
|
||||
dialogTextFieldValue: properties.dialogTextFieldValue,
|
||||
onPropertyChange: properties.onPropertyChange,
|
||||
disabled: properties.disabled,
|
||||
strings: properties.strings,
|
||||
onRender: this.onRender.bind(this),
|
||||
key: targetProperty
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the QueryFilterPanel property pane
|
||||
*****************************************************************************************/
|
||||
public render(): void {
|
||||
if (!this.elem) {
|
||||
return;
|
||||
}
|
||||
this.onRender(this.elem);
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Renders the QueryFilterPanel property pane
|
||||
*****************************************************************************************/
|
||||
private onRender(elem: HTMLElement): void {
|
||||
if (!this.elem) {
|
||||
this.elem = elem;
|
||||
}
|
||||
|
||||
const textDialog: React.ReactElement<ITextDialogProps> = React.createElement(TextDialog, {
|
||||
dialogTextFieldValue: this.properties.dialogTextFieldValue,
|
||||
onChanged: this.onChanged.bind(this),
|
||||
disabled: this.properties.disabled,
|
||||
strings: this.properties.strings,
|
||||
// required to allow the component to be re-rendered by calling this.render() externally
|
||||
stateKey: new Date().toString()
|
||||
});
|
||||
|
||||
ReactDom.render(textDialog, elem);
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************************
|
||||
* Call the property pane's onPropertyChange when the TextDialog changes
|
||||
*****************************************************************************************/
|
||||
private onChanged(text: string): void {
|
||||
this.properties.onPropertyChange(this.targetProperty, text);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.ace_editor.ace_autocomplete {
|
||||
z-index: 2000000 !important;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ITextDialogStrings } from "./ITextDialogStrings";
|
||||
|
||||
export interface ITextDialogProps {
|
||||
dialogTextFieldValue?: string;
|
||||
onChanged?: (text: string) => void;
|
||||
disabled?: boolean;
|
||||
strings: ITextDialogStrings;
|
||||
stateKey?: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface ITextDialogState {
|
||||
dialogText: string;
|
||||
showDialog: boolean;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface ITextDialogStrings {
|
||||
dialogTitle: string;
|
||||
dialogSubText?: string;
|
||||
dialogButtonLabel?: string;
|
||||
dialogButtonText: string;
|
||||
dialogTextBoxPlaceholder?: string;
|
||||
saveButtonText: string;
|
||||
cancelButtonText: string;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.textDialog {
|
||||
max-width: 100%;
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import * as React from 'react';
|
||||
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react';
|
||||
import { Button, ButtonType, Label } from 'office-ui-fabric-react';
|
||||
import { TextField } from 'office-ui-fabric-react';
|
||||
import { ITextDialogProps } from './ITextDialogProps';
|
||||
import { ITextDialogState } from './ITextDialogState';
|
||||
import AceEditor from 'react-ace';
|
||||
import styles from './TextDialog.module.scss';
|
||||
import './AceEditor.module.scss';
|
||||
|
||||
import 'brace';
|
||||
import 'brace/mode/html';
|
||||
import 'brace/theme/monokai';
|
||||
import 'brace/ext/language_tools';
|
||||
|
||||
export class TextDialog extends React.Component<ITextDialogProps, ITextDialogState> {
|
||||
|
||||
/*************************************************************************************
|
||||
* Component's constructor
|
||||
* @param props
|
||||
* @param state
|
||||
*************************************************************************************/
|
||||
constructor(props: ITextDialogProps, state: ITextDialogState) {
|
||||
super(props);
|
||||
this.state = { dialogText: this.props.dialogTextFieldValue, showDialog: false };
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Shows the dialog
|
||||
*************************************************************************************/
|
||||
private showDialog() {
|
||||
this.setState({ dialogText: this.state.dialogText, showDialog: true });
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Notifies the parent with the dialog's latest value, then closes the dialog
|
||||
*************************************************************************************/
|
||||
private saveDialog() {
|
||||
this.setState({ dialogText: this.state.dialogText, showDialog: false });
|
||||
|
||||
if(this.props.onChanged) {
|
||||
this.props.onChanged(this.state.dialogText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Closes the dialog without notifying the parent for any changes
|
||||
*************************************************************************************/
|
||||
private cancelDialog() {
|
||||
this.setState({ dialogText: this.state.dialogText, showDialog: false });
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Updates the dialog's value each time the textfield changes
|
||||
*************************************************************************************/
|
||||
private onDialogTextChanged(newValue: string) {
|
||||
this.setState({ dialogText: newValue, showDialog: this.state.showDialog });
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called immediately after updating occurs
|
||||
*************************************************************************************/
|
||||
public componentDidUpdate(prevProps: ITextDialogProps, prevState: ITextDialogState): void {
|
||||
if (this.props.disabled !== prevProps.disabled || this.props.stateKey !== prevProps.stateKey) {
|
||||
this.setState({ dialogText: this.props.dialogTextFieldValue, showDialog: this.state.showDialog });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Renders the the TextDialog component
|
||||
*************************************************************************************/
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<Label>{ this.props.strings.dialogButtonLabel }</Label>
|
||||
|
||||
<Button label={ this.props.strings.dialogButtonLabel } onClick={ this.showDialog.bind(this) }>{ this.props.strings.dialogButtonText }</Button>
|
||||
|
||||
<Dialog type={ DialogType.normal }
|
||||
isOpen={ this.state.showDialog }
|
||||
onDismiss={ this.cancelDialog.bind(this) }
|
||||
title={ this.props.strings.dialogTitle }
|
||||
subText={ this.props.strings.dialogSubText }
|
||||
isBlocking={ true }
|
||||
containerClassName={ 'ms-dialogMainOverride ' + styles.textDialog }>
|
||||
|
||||
<AceEditor
|
||||
width="100%"
|
||||
mode="html"
|
||||
theme="monokai"
|
||||
enableLiveAutocompletion={ true }
|
||||
showPrintMargin={ false }
|
||||
onChange={ this.onDialogTextChanged.bind(this) }
|
||||
value={ this.state.dialogText }
|
||||
name="CodeEditor"
|
||||
editorProps={{$blockScrolling: 0}} />
|
||||
|
||||
<DialogFooter>
|
||||
<Button buttonType={ ButtonType.primary } onClick={ this.saveDialog.bind(this) }>{ this.props.strings.saveButtonText }</Button>
|
||||
<Button onClick={ this.cancelDialog.bind(this) }>{ this.props.strings.cancelButtonText }</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||
|
||||
"id": "46edf08f-95c7-4ca7-9146-6471f9f471be",
|
||||
"alias": "ContentQueryWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "1.0.1",
|
||||
"manifestVersion": 2,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "46edf08f-95c7-4ca7-9146-6471f9f471be",
|
||||
"group": { "default": "SPP Technologies" },
|
||||
"title": { "default": "React Content Query WebPart" },
|
||||
"description": {
|
||||
"default": "A react content by query WebPart for querying items within a site and easily displaying them using a simple yet powerfull HandleBars templating engine.",
|
||||
"fr-FR": "Une composante React permettant d'effectuer des requêtes sur les items et de facilement afficher les résultat à l'aide de gabarits HandleBars fournit par l'utilisateur"
|
||||
},
|
||||
"officeFabricIconFontName": "Page",
|
||||
"properties": {
|
||||
"description": "React Content Query WebPart"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,490 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import * as strings from 'contentQueryStrings';
|
||||
import { Version, Text, Log } from '@microsoft/sp-core-library';
|
||||
import { BaseClientSideWebPart, IPropertyPaneConfiguration, IPropertyPaneField } from '@microsoft/sp-webpart-base';
|
||||
import { PropertyPaneTextField, IPropertyPaneTextFieldProps } from '@microsoft/sp-webpart-base';
|
||||
import { PropertyPaneChoiceGroup, IPropertyPaneChoiceGroupProps } from '@microsoft/sp-webpart-base';
|
||||
import { PropertyPaneToggle, IPropertyPaneToggleProps } from '@microsoft/sp-webpart-base';
|
||||
import { update, get, isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import { IDropdownOption, IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
import ContentQuery from './components/ContentQuery';
|
||||
import { IContentQueryProps } from './components/IContentQueryProps';
|
||||
import { IQuerySettings } from './components/IQuerySettings';
|
||||
import { IContentQueryTemplateContext } from './components/IContentQueryTemplateContext';
|
||||
import { IContentQueryWebPartProps } from './IContentQueryWebPartProps';
|
||||
import { PropertyPaneAsyncDropdown } from '../../controls/PropertyPaneAsyncDropdown/PropertyPaneAsyncDropdown';
|
||||
import { PropertyPaneQueryFilterPanel } from '../../controls/PropertyPaneQueryFilterPanel/PropertyPaneQueryFilterPanel';
|
||||
import { PropertyPaneAsyncChecklist } from '../../controls/PropertyPaneAsyncChecklist/PropertyPaneAsyncChecklist';
|
||||
import { PropertyPaneTextDialog } from '../../controls/PropertyPaneTextDialog/PropertyPaneTextDialog';
|
||||
import { IQueryFilterField } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilterField';
|
||||
import { IChecklistItem } from '../../controls/PropertyPaneAsyncChecklist/components/AsyncChecklist/IChecklistItem';
|
||||
import { ContentQueryService } from '../../common/services/ContentQueryService';
|
||||
import { IContentQueryService } from '../../common/services/IContentQueryService';
|
||||
import { ContentQueryConstants } from '../../common/constants/ContentQueryConstants';
|
||||
|
||||
|
||||
|
||||
export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQueryWebPartProps> {
|
||||
|
||||
private readonly logSource = "ContentQueryWebPart.ts";
|
||||
|
||||
/***************************************************************************
|
||||
* Service used to perform REST calls
|
||||
***************************************************************************/
|
||||
private ContentQueryService: IContentQueryService;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Custom ToolPart Property Panes
|
||||
***************************************************************************/
|
||||
private webUrlDropdown: PropertyPaneAsyncDropdown;
|
||||
private listTitleDropdown: PropertyPaneAsyncDropdown;
|
||||
private orderByDropdown: PropertyPaneAsyncDropdown;
|
||||
private orderByDirectionChoiceGroup: IPropertyPaneField<IPropertyPaneChoiceGroupProps>;
|
||||
private limitEnabledToggle: IPropertyPaneField<IPropertyPaneToggleProps>;
|
||||
private itemLimitTextField: IPropertyPaneField<IPropertyPaneTextFieldProps>;
|
||||
private filtersPanel: PropertyPaneQueryFilterPanel;
|
||||
private viewFieldsChecklist: PropertyPaneAsyncChecklist;
|
||||
private templateTextDialog: PropertyPaneTextDialog;
|
||||
private templateUrlTextField: IPropertyPaneField<IPropertyPaneTextFieldProps>;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the WebPart's version
|
||||
***************************************************************************/
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0.2');
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Initializes the WebPart
|
||||
***************************************************************************/
|
||||
protected onInit(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.ContentQueryService = new ContentQueryService(this.context, this.context.spHttpClient);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Renders the WebPart
|
||||
***************************************************************************/
|
||||
public render(): void {
|
||||
let querySettings: IQuerySettings = {
|
||||
webUrl: this.properties.webUrl,
|
||||
listTitle: this.properties.listTitle,
|
||||
limitEnabled: this.properties.limitEnabled,
|
||||
itemLimit: this.properties.itemLimit,
|
||||
orderBy: this.properties.orderBy,
|
||||
orderByDirection: this.properties.orderByDirection,
|
||||
filters: this.properties.filters,
|
||||
viewFields: this.properties.viewFields,
|
||||
};
|
||||
|
||||
const element: React.ReactElement<IContentQueryProps> = React.createElement(ContentQuery,
|
||||
{
|
||||
onLoadTemplate: this.loadTemplate.bind(this),
|
||||
onLoadTemplateContext: this.loadTemplateContext.bind(this),
|
||||
querySettings: querySettings,
|
||||
templateText: this.properties.templateText,
|
||||
templateUrl: this.properties.templateUrl,
|
||||
strings: strings.contentQueryStrings,
|
||||
stateKey: new Date().toString()
|
||||
}
|
||||
);
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the toolpart configuration
|
||||
***************************************************************************/
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
|
||||
let firstCascadingLevelDisabled = !this.properties.webUrl;
|
||||
let secondCascadingLevelDisabled = !this.properties.webUrl || !this.properties.listTitle;
|
||||
|
||||
// Creates a custom PropertyPaneAsyncDropdown for the webUrl property
|
||||
this.webUrlDropdown = new PropertyPaneAsyncDropdown(ContentQueryConstants.propertyWebUrl, {
|
||||
label: strings.WebUrlFieldLabel,
|
||||
loadingLabel: strings.WebUrlFieldLoadingLabel,
|
||||
errorLabelFormat: strings.WebUrlFieldLoadingError,
|
||||
loadOptions: this.loadWebUrlOptions.bind(this),
|
||||
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
|
||||
selectedKey: this.properties.webUrl || ""
|
||||
});
|
||||
|
||||
// Creates a custom PropertyPaneAsyncDropdown for the listTitle property
|
||||
this.listTitleDropdown = new PropertyPaneAsyncDropdown(ContentQueryConstants.propertyListTitle, {
|
||||
label: strings.ListTitleFieldLabel,
|
||||
loadingLabel: strings.ListTitleFieldLoadingLabel,
|
||||
errorLabelFormat: strings.ListTitleFieldLoadingError,
|
||||
loadOptions: this.loadListTitleOptions.bind(this),
|
||||
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
|
||||
selectedKey: this.properties.listTitle || "",
|
||||
disabled: firstCascadingLevelDisabled
|
||||
});
|
||||
|
||||
// Creates a custom PropertyPaneAsyncDropdown for the orderBy property
|
||||
this.orderByDropdown = new PropertyPaneAsyncDropdown(ContentQueryConstants.propertyOrderBy, {
|
||||
label: strings.OrderByFieldLabel,
|
||||
loadingLabel: strings.OrderByFieldLoadingLabel,
|
||||
errorLabelFormat: strings.OrderByFieldLoadingError,
|
||||
loadOptions: this.loadOrderByOptions.bind(this),
|
||||
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
|
||||
selectedKey: this.properties.orderBy || "",
|
||||
disabled: secondCascadingLevelDisabled
|
||||
});
|
||||
|
||||
// Creates a custom PropertyPaneQueryFilterPanel for the filters property
|
||||
this.filtersPanel = new PropertyPaneQueryFilterPanel(ContentQueryConstants.propertyFilters, {
|
||||
filters: this.properties.filters,
|
||||
loadFields: this.loadFilterFields.bind(this),
|
||||
onLoadTaxonomyPickerSuggestions: this.loadTaxonomyPickerSuggestions.bind(this),
|
||||
onLoadPeoplePickerSuggestions: this.loadPeoplePickerSuggestions.bind(this),
|
||||
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
|
||||
trimEmptyFiltersOnChange: true,
|
||||
disabled: secondCascadingLevelDisabled,
|
||||
strings: strings.queryFilterPanelStrings
|
||||
});
|
||||
|
||||
// Creates a custom PropertyPaneAsyncChecklist for the viewFields property
|
||||
this.viewFieldsChecklist = new PropertyPaneAsyncChecklist(ContentQueryConstants.propertyViewFields, {
|
||||
loadItems: this.loadViewFieldsChecklistItems.bind(this),
|
||||
checkedItems: this.properties.viewFields,
|
||||
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
|
||||
disable: secondCascadingLevelDisabled,
|
||||
strings: strings.viewFieldsChecklistStrings
|
||||
});
|
||||
|
||||
// Creates a custom PropertyPaneTextDialog for the templateText property
|
||||
this.templateTextDialog = new PropertyPaneTextDialog(ContentQueryConstants.propertyTemplateText, {
|
||||
dialogTextFieldValue: this.properties.templateText,
|
||||
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
|
||||
disabled: false,
|
||||
strings: strings.templateTextStrings
|
||||
});
|
||||
|
||||
// Creates a PropertyPaneChoiceGroup for the orderByDirection property
|
||||
this.orderByDirectionChoiceGroup = PropertyPaneChoiceGroup(ContentQueryConstants.propertOrderByDirection, {
|
||||
options: [
|
||||
{ text: strings.ShowItemsAscending, key: 'asc', checked: !this.properties.orderByDirection || this.properties.orderByDirection == 'asc', disabled: secondCascadingLevelDisabled },
|
||||
{ text: strings.ShowItemsDescending, key: 'desc', checked: this.properties.orderByDirection == 'desc', disabled: secondCascadingLevelDisabled }
|
||||
]
|
||||
});
|
||||
|
||||
// Creates a PropertyPaneTextField for the templateUrl property
|
||||
this.templateUrlTextField = PropertyPaneTextField(ContentQueryConstants.propertyTemplateUrl, {
|
||||
label: strings.TemplateUrlFieldLabel,
|
||||
placeholder: strings.TemplateUrlPlaceholder,
|
||||
deferredValidationTime: 500,
|
||||
onGetErrorMessage: this.onTemplateUrlChange.bind(this)
|
||||
});
|
||||
|
||||
// Creates a PropertyPaneToggle for the limitEnabled property
|
||||
this.limitEnabledToggle = PropertyPaneToggle(ContentQueryConstants.propertyLimitEnabled, {
|
||||
label: strings.LimitEnabledFieldLabel,
|
||||
offText: 'Disabled',
|
||||
onText: 'Enabled',
|
||||
checked: this.properties.limitEnabled,
|
||||
disabled: secondCascadingLevelDisabled
|
||||
});
|
||||
|
||||
// Creates a PropertyPaneTextField for the itemLimit property
|
||||
this.itemLimitTextField = PropertyPaneTextField(ContentQueryConstants.propertyItemLimit, {
|
||||
deferredValidationTime: 500,
|
||||
placeholder: strings.ItemLimitPlaceholder,
|
||||
disabled: !this.properties.limitEnabled || secondCascadingLevelDisabled,
|
||||
onGetErrorMessage: this.onItemLimitChange.bind(this)
|
||||
});
|
||||
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: { description: strings.SourcePageDescription },
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.SourceGroupName,
|
||||
groupFields: [
|
||||
this.webUrlDropdown,
|
||||
this.listTitleDropdown
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
header: { description: strings.QueryPageDescription },
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.QueryGroupName,
|
||||
groupFields: [
|
||||
this.orderByDropdown,
|
||||
this.orderByDirectionChoiceGroup,
|
||||
this.limitEnabledToggle,
|
||||
this.itemLimitTextField,
|
||||
this.filtersPanel
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
header: { description: strings.DisplayPageDescription },
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.DisplayGroupName,
|
||||
groupFields: [
|
||||
this.viewFieldsChecklist,
|
||||
this.templateTextDialog,
|
||||
this.templateUrlTextField
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the HandleBars template from the specified url
|
||||
***************************************************************************/
|
||||
private loadTemplate(templateUrl:string): Promise<string> {
|
||||
return this.ContentQueryService.getFileContent(templateUrl);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the HandleBars context based on the specified query
|
||||
***************************************************************************/
|
||||
private loadTemplateContext(querySettings:IQuerySettings, callTimeStamp: number): Promise<IContentQueryTemplateContext> {
|
||||
return this.ContentQueryService.getTemplateContext(querySettings, callTimeStamp);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the dropdown options for the webUrl property
|
||||
***************************************************************************/
|
||||
private loadWebUrlOptions(): Promise<IDropdownOption[]> {
|
||||
return this.ContentQueryService.getWebUrlOptions();
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the dropdown options for the listTitle property
|
||||
***************************************************************************/
|
||||
private loadListTitleOptions(): Promise<IDropdownOption[]> {
|
||||
return this.ContentQueryService.getListTitleOptions(this.properties.webUrl);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the dropdown options for the orderBy property
|
||||
***************************************************************************/
|
||||
private loadOrderByOptions(): Promise<IDropdownOption[]> {
|
||||
return this.ContentQueryService.getOrderByOptions(this.properties.webUrl, this.properties.listTitle);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the dropdown options for the listTitle property
|
||||
***************************************************************************/
|
||||
private loadFilterFields():Promise<IQueryFilterField[]> {
|
||||
return this.ContentQueryService.getFilterFields(this.properties.webUrl, this.properties.listTitle);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Loads the checklist items for the viewFields property
|
||||
***************************************************************************/
|
||||
private loadViewFieldsChecklistItems():Promise<IChecklistItem[]> {
|
||||
return this.ContentQueryService.getViewFieldsChecklistItems(this.properties.webUrl, this.properties.listTitle);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the user suggestions based on the user entered picker input
|
||||
* @param filterText : The filter specified by the user in the people picker
|
||||
* @param currentPersonas : The IPersonaProps already selected in the people picker
|
||||
* @param limitResults : The results limit if any
|
||||
***************************************************************************/
|
||||
private loadPeoplePickerSuggestions(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number):Promise<IPersonaProps[]> {
|
||||
return this.ContentQueryService.getPeoplePickerSuggestions(this.properties.webUrl, filterText, currentPersonas, limitResults);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the taxonomy suggestions based on the user entered picker input
|
||||
* @param field : The taxonomy field from which to load the terms from
|
||||
* @param filterText : The filter specified by the user in the people picker
|
||||
* @param currentPersonas : The IPersonaProps already selected in the people picker
|
||||
* @param limitResults : The results limit if any
|
||||
***************************************************************************/
|
||||
private loadTaxonomyPickerSuggestions(field: IQueryFilterField, filterText: string, currentTerms: ITag[]):Promise<ITag[]> {
|
||||
return this.ContentQueryService.getTaxonomyPickerSuggestions(this.properties.webUrl, this.properties.listTitle, field, filterText, currentTerms);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* When a custom property pane updates
|
||||
***************************************************************************/
|
||||
private onCustomPropertyPaneChange(propertyPath: string, newValue: any): void {
|
||||
Log.verbose(this.logSource, "WebPart property '" + propertyPath + "' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
let rerenderTemplateTextDialog = false;
|
||||
const oldValue = get(this.properties, propertyPath);
|
||||
|
||||
// Stores the new value in web part properties
|
||||
update(this.properties, propertyPath, (): any => { return newValue; });
|
||||
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
|
||||
// Resets dependent property panes if needed
|
||||
this.resetDependentPropertyPanes(propertyPath);
|
||||
|
||||
// If the viewfields have changed, update the default template text if it hasn't been altered by the user
|
||||
if(propertyPath == ContentQueryConstants.propertyViewFields && !this.properties.hasDefaultTemplateBeenUpdated) {
|
||||
let generatedTemplate = this.ContentQueryService.generateDefaultTemplate(newValue);
|
||||
update(this.properties, ContentQueryConstants.propertyTemplateText, (): any => { return generatedTemplate; });
|
||||
this.templateTextDialog.properties.dialogTextFieldValue = generatedTemplate;
|
||||
rerenderTemplateTextDialog = true;
|
||||
}
|
||||
|
||||
// If the templateText have changed, update the "hasDefaultTemplateBeenUpdated" to true so the WebPart doesn't override the user template after updating view fields
|
||||
if(propertyPath == ContentQueryConstants.propertyTemplateText && !this.properties.hasDefaultTemplateBeenUpdated) {
|
||||
update(this.properties, ContentQueryConstants.propertyhasDefaultTemplateBeenUpdated, (): any => { return true; });
|
||||
}
|
||||
|
||||
// Refreshes the web part manually because custom fields don't update since sp-webpart-base@1.1.1
|
||||
// https://github.com/SharePoint/sp-dev-docs/issues/594
|
||||
if (!this.disableReactivePropertyChanges)
|
||||
this.render();
|
||||
|
||||
if(rerenderTemplateTextDialog) {
|
||||
this.templateTextDialog.render();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets dependent property panes if needed
|
||||
***************************************************************************/
|
||||
private resetDependentPropertyPanes(propertyPath: string): void {
|
||||
if(propertyPath == ContentQueryConstants.propertyWebUrl) {
|
||||
this.resetListTitlePropertyPane();
|
||||
this.resetOrderByPropertyPane();
|
||||
this.resetFiltersPropertyPane();
|
||||
this.resetViewFieldsPropertyPane();
|
||||
}
|
||||
else if (propertyPath == ContentQueryConstants.propertyListTitle) {
|
||||
this.resetOrderByPropertyPane();
|
||||
this.resetFiltersPropertyPane();
|
||||
this.resetViewFieldsPropertyPane();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Validates the templateUrl property
|
||||
***************************************************************************/
|
||||
private onTemplateUrlChange(value: string): Promise<String> {
|
||||
Log.verbose(this.logSource, "WebPart property 'templateUrl' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
|
||||
// Doesn't raise any error if file is empty (otherwise error message will show on initial load...)
|
||||
if(isEmpty(value)) {
|
||||
resolve('');
|
||||
}
|
||||
// Resolves an error if the file isn't a valid .htm or .html file
|
||||
else if(!this.ContentQueryService.isValidTemplateFile(value)) {
|
||||
resolve(strings.ErrorTemplateExtension);
|
||||
}
|
||||
// Resolves an error if the file doesn't answer a simple head request
|
||||
else {
|
||||
this.ContentQueryService.ensureFileResolves(value).then((isFileResolving:boolean) => {
|
||||
resolve('');
|
||||
})
|
||||
.catch((error) => {
|
||||
resolve(Text.format(strings.ErrorTemplateResolve, error));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Validates the itemLimit property
|
||||
***************************************************************************/
|
||||
private onItemLimitChange(value: string): Promise<String> {
|
||||
Log.verbose(this.logSource, "WebPart property 'itemLimit' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
// Resolves an error if the file isn't a valid number between 1 to 999
|
||||
let parsedValue = parseInt(value);
|
||||
let isNumeric = !isNaN(parsedValue) && isFinite(parsedValue);
|
||||
let isValid = (isNumeric && parsedValue >= 1 && parsedValue <= 999) || isEmpty(value);
|
||||
resolve(!isValid ? strings.ErrorItemLimit : '');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the List Title property pane and re-renders it
|
||||
***************************************************************************/
|
||||
private resetListTitlePropertyPane() {
|
||||
Log.verbose(this.logSource, "Resetting 'listTitle' property...", this.context.serviceScope);
|
||||
|
||||
this.properties.listTitle = null;
|
||||
this.ContentQueryService.clearCachedListTitleOptions();
|
||||
update(this.properties, ContentQueryConstants.propertyListTitle, (): any => { return this.properties.listTitle; });
|
||||
this.listTitleDropdown.properties.selectedKey = "";
|
||||
this.listTitleDropdown.properties.disabled = isEmpty(this.properties.webUrl);
|
||||
this.listTitleDropdown.render();
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the Filters property pane and re-renders it
|
||||
***************************************************************************/
|
||||
private resetOrderByPropertyPane() {
|
||||
Log.verbose(this.logSource, "Resetting 'orderBy' property...", this.context.serviceScope);
|
||||
|
||||
this.properties.orderBy = null;
|
||||
this.ContentQueryService.clearCachedOrderByOptions();
|
||||
update(this.properties, ContentQueryConstants.propertyOrderBy, (): any => { return this.properties.orderBy; });
|
||||
this.orderByDropdown.properties.selectedKey = "";
|
||||
this.orderByDropdown.properties.disabled = isEmpty(this.properties.webUrl) || isEmpty(this.properties.listTitle);
|
||||
this.orderByDropdown.render();
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the Filters property pane and re-renders it
|
||||
***************************************************************************/
|
||||
private resetFiltersPropertyPane() {
|
||||
Log.verbose(this.logSource, "Resetting 'filters' property...", this.context.serviceScope);
|
||||
|
||||
this.properties.filters = null;
|
||||
this.ContentQueryService.clearCachedFilterFields();
|
||||
update(this.properties, ContentQueryConstants.propertyFilters, (): any => { return this.properties.filters; });
|
||||
this.filtersPanel.properties.filters = null;
|
||||
this.filtersPanel.properties.disabled = isEmpty(this.properties.webUrl) || isEmpty(this.properties.listTitle);
|
||||
this.filtersPanel.render();
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Resets the View Fields property pane and re-renders it
|
||||
***************************************************************************/
|
||||
private resetViewFieldsPropertyPane() {
|
||||
Log.verbose(this.logSource, "Resetting 'viewFields' property...", this.context.serviceScope);
|
||||
|
||||
this.properties.viewFields = null;
|
||||
this.ContentQueryService.clearCachedViewFields();
|
||||
update(this.properties, ContentQueryConstants.propertyViewFields, (): any => { return this.properties.viewFields; });
|
||||
this.viewFieldsChecklist.properties.checkedItems = null;
|
||||
this.viewFieldsChecklist.properties.disable = isEmpty(this.properties.webUrl) || isEmpty(this.properties.listTitle);
|
||||
this.viewFieldsChecklist.render();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IQueryFilter } from "../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilter";
|
||||
|
||||
export interface IContentQueryWebPartProps {
|
||||
webUrl: string;
|
||||
listTitle: string;
|
||||
limitEnabled: boolean;
|
||||
itemLimit: number;
|
||||
orderBy: string;
|
||||
orderByDirection: string;
|
||||
filters: IQueryFilter[];
|
||||
viewFields: string[];
|
||||
templateText: string;
|
||||
templateUrl: string;
|
||||
hasDefaultTemplateBeenUpdated: boolean;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
$container-bg-color: #f4f4f4;
|
||||
$container-border: 1px solid #dadada;
|
||||
|
||||
.cqwp {
|
||||
.cqwpValidations {
|
||||
background-color: $container-bg-color;
|
||||
border: $container-border;
|
||||
padding: 20px 20px 15px 20px;
|
||||
|
||||
div[class*='ms-Checkbox'] {
|
||||
&:first-child {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cqwpError {
|
||||
background-color: $container-bg-color;
|
||||
border: $container-border;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
import * as React from 'react';
|
||||
import * as Handlebars from "handlebars";
|
||||
import * as strings from 'contentQueryStrings';
|
||||
import { Checkbox, Spinner } from 'office-ui-fabric-react';
|
||||
import { isEmpty } from '@microsoft/sp-lodash-subset';
|
||||
import { Text } from '@microsoft/sp-core-library';
|
||||
import { IContentQueryProps } from './IContentQueryProps';
|
||||
import { IContentQueryState } from './IContentQueryState';
|
||||
import { IContentQueryTemplateContext } from './IContentQueryTemplateContext';
|
||||
import styles from './ContentQuery.module.scss';
|
||||
|
||||
|
||||
export default class ContentQuery extends React.Component<IContentQueryProps, IContentQueryState> {
|
||||
|
||||
/*************************************************************************************
|
||||
* Stores the timestamps of each async calls in order to wait for the last call in
|
||||
* case multiple calls have been fired in a short lapse of time by updaing the
|
||||
* toolpane too fast
|
||||
*************************************************************************************/
|
||||
private onGoingAsyncCalls: number[];
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Component's constructor
|
||||
* @param props
|
||||
* @param state
|
||||
*************************************************************************************/
|
||||
constructor(props: IContentQueryProps, state: IContentQueryState) {
|
||||
super(props);
|
||||
|
||||
// Imports the handlebars-helpers
|
||||
let helpers = require<any>('handlebars-helpers')({
|
||||
handlebars: Handlebars
|
||||
});
|
||||
|
||||
this.onGoingAsyncCalls = [];
|
||||
this.state = { loading: true, processedTemplateResult: null, error: null };
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns whether the specified call is the LAST executed call within the stored calls
|
||||
*************************************************************************************/
|
||||
private isLastExecutedCall(timeStamp: number) {
|
||||
return (this.onGoingAsyncCalls.length > 0 && this.onGoingAsyncCalls.filter((t: number) => { return t > timeStamp; }).length == 0);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Loads the items asynchronously and wraps them into a context object for handlebars
|
||||
*************************************************************************************/
|
||||
private loadTemplateContext() {
|
||||
|
||||
if(this.areMandatoryFieldsConfigured()) {
|
||||
|
||||
// Stores the current call timestamp locally
|
||||
let currentCallTimeStamp = new Date().valueOf();
|
||||
this.onGoingAsyncCalls.push(currentCallTimeStamp);
|
||||
|
||||
// Resets the state if this is the first call
|
||||
if(this.onGoingAsyncCalls.length == 1) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
processedTemplateResult: null,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
// Fires the async call with its associated timestamp
|
||||
this.props.onLoadTemplateContext(this.props.querySettings, currentCallTimeStamp).then((templateContext: IContentQueryTemplateContext) => {
|
||||
|
||||
// Loads the handlebars template
|
||||
this.loadTemplate().then((templateContent: string) => {
|
||||
|
||||
// Only process the result of the current async call if it's the last in the ordered queue
|
||||
if(this.isLastExecutedCall(templateContext.callTimeStamp)) {
|
||||
|
||||
// Resets the onGoingAsyncCalls
|
||||
this.onGoingAsyncCalls = [];
|
||||
|
||||
// Process the handlebars template
|
||||
this.processTemplate(templateContent, templateContext);
|
||||
}
|
||||
})
|
||||
.catch((error: string) => {
|
||||
this.setState({ loading: false, processedTemplateResult: null, error: Text.format(this.props.strings.errorLoadingTemplate, error) });
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({ loading: false, processedTemplateResult: null, error: Text.format(this.props.strings.errorLoadingQuery, error) });
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.setState({ loading: false, processedTemplateResult: null, error: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Loads the template from url if available, otherwise returns the inline template
|
||||
*************************************************************************************/
|
||||
private loadTemplate(): Promise<string> {
|
||||
// Resolves the template content if no template url
|
||||
if(isEmpty(this.props.templateUrl)) {
|
||||
return Promise.resolve(this.props.templateText);
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve,reject) => {
|
||||
this.props.onLoadTemplate(this.props.templateUrl).then((templateContent: string) => {
|
||||
resolve(templateContent);
|
||||
})
|
||||
.catch((error: string) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Process the specified handlebars template with the given template context
|
||||
* @param templateContent : The handlebars template that needs to be compiled
|
||||
* @param templateContext : The context that must be applied to the compiled template
|
||||
*************************************************************************************/
|
||||
private processTemplate(templateContent: string, templateContext: IContentQueryTemplateContext) {
|
||||
try {
|
||||
// Processes the template
|
||||
let template = Handlebars.compile(templateContent);
|
||||
let result = template(templateContext);
|
||||
|
||||
// Updates the state only if the stored calls are still empty (just in case they get updated during the processing of the handlebars template)
|
||||
if(this.onGoingAsyncCalls.length == 0) {
|
||||
this.setState({ loading: false, processedTemplateResult: result, error: null });
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
this.setState({ loading: false, processedTemplateResult: null, error: Text.format(this.props.strings.errorProcessingTemplate, error) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Returns whether all mandatory fields are configured or not
|
||||
*************************************************************************************/
|
||||
private areMandatoryFieldsConfigured(): boolean {
|
||||
return !isEmpty(this.props.querySettings.webUrl) &&
|
||||
!isEmpty(this.props.querySettings.listTitle) &&
|
||||
!isEmpty(this.props.querySettings.viewFields) &&
|
||||
(!isEmpty(this.props.templateUrl) || !isEmpty(this.props.templateText));
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Converts the specified HTML by an object required for dangerouslySetInnerHTML
|
||||
* @param html
|
||||
*************************************************************************************/
|
||||
private createMarkup(html: string) {
|
||||
return {__html: html};
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Called once after initial rendering
|
||||
*************************************************************************************/
|
||||
public componentDidMount(): void {
|
||||
this.loadTemplateContext();
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Gets called when the WebPart refreshes (because of the reactive mode for instance)
|
||||
*************************************************************************************/
|
||||
public componentDidUpdate(prevProps: IContentQueryProps, prevState: IContentQueryState): void {
|
||||
if(prevProps.stateKey !== this.props.stateKey) {
|
||||
this.loadTemplateContext();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* Renders the Content by Query WebPart
|
||||
*************************************************************************************/
|
||||
public render(): React.ReactElement<IContentQueryProps> {
|
||||
|
||||
const loading = this.state.loading ? <Spinner label={this.props.strings.loadingItems} /> : <div />;
|
||||
const error = this.state.error ? <div className={styles.cqwpError}>{this.state.error}</div> : <div />;
|
||||
const mandatoryFieldsConfigured = this.areMandatoryFieldsConfigured();
|
||||
|
||||
return (
|
||||
<div className={styles.cqwp}>
|
||||
|
||||
{loading}
|
||||
|
||||
{error}
|
||||
|
||||
{/* Shows the validation checklist if mandatory properties aren't all configured */}
|
||||
{ !mandatoryFieldsConfigured && !this.state.loading && !this.state.error &&
|
||||
<div className={styles.cqwpValidations}>
|
||||
{ this.props.strings.mandatoryProperties }
|
||||
|
||||
<Checkbox label={strings.WebUrlFieldLabel} checked={!isEmpty(this.props.querySettings.webUrl)} />
|
||||
<Checkbox label={strings.ListTitleFieldLabel} checked={!isEmpty(this.props.querySettings.listTitle)} />
|
||||
<Checkbox label={strings.viewFieldsChecklistStrings.label} checked={!isEmpty(this.props.querySettings.viewFields)} />
|
||||
<Checkbox label={strings.templateTextStrings.dialogButtonLabel + " / " + strings.TemplateUrlFieldLabel} checked={(!isEmpty(this.props.templateUrl) || !isEmpty(this.props.templateText))} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* Shows the query results once loaded */}
|
||||
{ mandatoryFieldsConfigured && !this.state.loading && !this.state.error &&
|
||||
<div dangerouslySetInnerHTML={ this.createMarkup(this.state.processedTemplateResult) }></div>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { IContentQueryTemplateContext } from './IContentQueryTemplateContext';
|
||||
import { IContentQueryStrings } from './IContentQueryStrings';
|
||||
import { IQuerySettings } from './IQuerySettings';
|
||||
|
||||
|
||||
export interface IContentQueryProps {
|
||||
onLoadTemplate: (templateUrl: string) => Promise<string>;
|
||||
onLoadTemplateContext: (querySettings: IQuerySettings, callTimeStamp: number) => Promise<IContentQueryTemplateContext>;
|
||||
querySettings: IQuerySettings;
|
||||
templateText?: string;
|
||||
templateUrl?: string;
|
||||
strings: IContentQueryStrings;
|
||||
stateKey: string;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface IContentQueryState {
|
||||
loading: boolean;
|
||||
processedTemplateResult: string;
|
||||
error: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface IContentQueryStrings {
|
||||
loadingItems: string;
|
||||
mandatoryProperties: string;
|
||||
errorLoadingQuery: string;
|
||||
errorLoadingTemplate: string;
|
||||
errorProcessingTemplate: string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { PageContext } from '@microsoft/sp-page-context';
|
||||
|
||||
export interface IContentQueryTemplateContext {
|
||||
pageContext: PageContext;
|
||||
items: any[];
|
||||
accessDenied: boolean;
|
||||
webNotFound: boolean;
|
||||
callTimeStamp: number;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { IQueryFilter } from '../../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilter';
|
||||
|
||||
export interface IQuerySettings {
|
||||
webUrl: string;
|
||||
listTitle: string;
|
||||
limitEnabled: boolean;
|
||||
itemLimit: number;
|
||||
orderBy: string;
|
||||
orderByDirection: string;
|
||||
filters: IQueryFilter[];
|
||||
viewFields: string[];
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
define([], function() {
|
||||
return {
|
||||
SourcePageDescription: "Specify where the WebPart should get the results from.",
|
||||
QueryPageDescription: "If needed, choose the sorting behavior, limit the results, or add filters in order to narrow the query down.",
|
||||
DisplayPageDescription: "Specify which fields should be available for rendering within the HandleBars template, and edit your handlebars template.",
|
||||
SourceGroupName: "Source",
|
||||
QueryGroupName: "Query",
|
||||
DisplayGroupName: "Display",
|
||||
WebUrlFieldLabel: "Web Url",
|
||||
WebUrlFieldPlaceholder: "Select the source web...",
|
||||
WebUrlFieldLoadingLabel: "Loading webs from current site...",
|
||||
WebUrlFieldLoadingError: "An error occured while loading webs : {0}",
|
||||
ListTitleFieldLabel: "List Title",
|
||||
ListTitleFieldPlaceholder: "Select the source list...",
|
||||
ListTitleFieldLoadingLabel: "Loading lists from specified web...",
|
||||
ListTitleFieldLoadingError: "An error occured while loading lists : {0}",
|
||||
OrderByFieldLabel: "Order By",
|
||||
OrderByFieldLoadingLabel: "Loading fields from specified list...",
|
||||
OrderByFieldLoadingError: "An error occured while loading fields : {0}",
|
||||
LimitEnabledFieldLabel: "Limit the number of items to display",
|
||||
ItemLimitPlaceholder: "Enter a limit from 1 to 999",
|
||||
ErrorItemLimit: "Value must be a number between 1 to 999",
|
||||
TemplateUrlFieldLabel: "Template Url",
|
||||
TemplateUrlPlaceholder: "Enter a valid HandleBars .htm file url",
|
||||
ErrorTemplateExtension: "The template must be a valid .htm or .html file",
|
||||
ErrorTemplateResolve: "Unable to resolve the specified template : {0}",
|
||||
ErrorWebAccessDenied: "You do not have access to the previously configured web url '{0}'. Either leave the WebPart properties as is or select another web url.",
|
||||
ErrorWebNotFound: "The previously configured web url '{0}' is not found anymore. Either leave the WebPart properties as is or select another web url.",
|
||||
ErrorProcessingTemplate: "An error occured while processing the handlebars template : {0}",
|
||||
ShowItemsAscending: "Show items in ascending order",
|
||||
ShowItemsDescending: "Show items in descending order",
|
||||
DynamicallyGeneratedTemplate: "Dynamically generated template",
|
||||
queryFilterPanelStrings: {
|
||||
filtersLabel: "Filters",
|
||||
addFilterLabel: "Add filter",
|
||||
loadingFieldsLabel: "Loading fields from specified list...",
|
||||
loadingFieldsErrorLabel: "An error occured while loading fields : {0}",
|
||||
queryFilterStrings: {
|
||||
fieldLabel: "Field",
|
||||
fieldSelectLabel: "Select a field...",
|
||||
operatorLabel: "Operator",
|
||||
operatorEqualLabel: 'Equals',
|
||||
operatorNotEqualLabel: 'Does not equal',
|
||||
operatorGreaterLabel: 'Is greater than',
|
||||
operatorGreaterEqualLabel: 'Is greater or equal to',
|
||||
operatorLessLabel: 'Is less than',
|
||||
operatorLessEqualLabel: 'Is less or equal to',
|
||||
operatorContainsLabel: 'Contains',
|
||||
operatorBeginsWithLabel: 'Begins with',
|
||||
operatorContainsAnyLabel: 'Contains Any',
|
||||
operatorContainsAllLabel: 'Contains All',
|
||||
operatorIsNullLabel: 'Is Null',
|
||||
operatorIsNotNullLabel: 'Is Not Null',
|
||||
valueLabel: 'Value',
|
||||
andLabel: 'And',
|
||||
orLabel: 'Or',
|
||||
peoplePickerSuggestionHeader: 'Suggested People',
|
||||
peoplePickerNoResults: 'No results found',
|
||||
peoplePickerLoading: 'Loading users',
|
||||
peoplePickerMe: 'Me',
|
||||
taxonomyPickerSuggestionHeader: 'Suggested Terms',
|
||||
taxonomyPickerNoResults: 'No results found',
|
||||
taxonomyPickerLoading: 'Loading terms',
|
||||
datePickerLocale: 'en',
|
||||
datePickerFormat: 'MMM Do YYYY, hh:mm a',
|
||||
datePickerExpressionError: 'Expression must respect the following format : [Today] or [Today] +/- [digit]',
|
||||
datePickerDatePlaceholder: 'Select a date...',
|
||||
datePickerExpressionPlaceholder: 'Or enter a valid expression...',
|
||||
datePickerIncludeTime: 'Include time in query',
|
||||
datePickerStrings: {
|
||||
months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
|
||||
shortMonths: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
|
||||
days: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ],
|
||||
shortDays: [ 'S', 'M', 'T', 'W', 'T', 'F', 'S' ],
|
||||
goToToday: 'Go to today'
|
||||
}
|
||||
}
|
||||
},
|
||||
viewFieldsChecklistStrings: {
|
||||
label: 'View Fields',
|
||||
loading: 'Loading fields from specified list...',
|
||||
errorFormat: 'An error occured while loading fields : {0}'
|
||||
},
|
||||
templateTextStrings: {
|
||||
dialogTitle: "Edit template content",
|
||||
dialogSubText: "Edit your handlebars template in this dialog. Note that the inline template specified within this dialog will be ignored if a template url is specified.",
|
||||
dialogButtonLabel: "Template",
|
||||
dialogButtonText: "Edit template content",
|
||||
dialogTextBoxPlaceholder: "Edit your handlebars template here...",
|
||||
saveButtonText: 'Save',
|
||||
cancelButtonText: 'Cancel'
|
||||
},
|
||||
contentQueryStrings: {
|
||||
loadingItems: 'Processing query',
|
||||
mandatoryProperties: 'Configure the following mandatory properties in order to display results :',
|
||||
errorLoadingQuery: 'An error occured while processing the query : {0}',
|
||||
errorLoadingTemplate: 'An error occured while loading the template: {0}',
|
||||
errorProcessingTemplate: 'An error occured while processing the handlebars template : {0}'
|
||||
}
|
||||
}
|
||||
});
|
40
samples/react-content-query-webpart/src/webparts/contentQuery/loc/mystrings.d.ts
vendored
Normal file
40
samples/react-content-query-webpart/src/webparts/contentQuery/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
declare interface IContentQueryStrings {
|
||||
SourcePageDescription: string;
|
||||
QueryPageDescription: string;
|
||||
DisplayPageDescription: string;
|
||||
SourceGroupName: string;
|
||||
QueryGroupName: string;
|
||||
DisplayGroupName: string;
|
||||
WebUrlFieldLabel: string;
|
||||
WebUrlFieldPlaceholder: string;
|
||||
WebUrlFieldLoadingLabel: string;
|
||||
WebUrlFieldLoadingError: string;
|
||||
ListTitleFieldLabel: string;
|
||||
ListTitleFieldPlaceholder: string;
|
||||
ListTitleFieldLoadingLabel: string;
|
||||
ListTitleFieldLoadingError: string;
|
||||
OrderByFieldLabel: string;
|
||||
OrderByFieldLoadingLabel: string;
|
||||
OrderByFieldLoadingError: string;
|
||||
LimitEnabledFieldLabel: string;
|
||||
ItemLimitPlaceholder: string;
|
||||
ErrorItemLimit: string;
|
||||
TemplateUrlFieldLabel: string;
|
||||
TemplateUrlPlaceholder: string;
|
||||
ErrorTemplateExtension: string;
|
||||
ErrorTemplateResolve: string;
|
||||
ErrorWebAccessDenied: string;
|
||||
ErrorWebNotFound: string;
|
||||
ShowItemsAscending: string;
|
||||
ShowItemsDescending: string;
|
||||
DynamicallyGeneratedTemplate: string;
|
||||
queryFilterPanelStrings: any;
|
||||
viewFieldsChecklistStrings: any;
|
||||
templateTextStrings: any;
|
||||
contentQueryStrings: any;
|
||||
}
|
||||
|
||||
declare module 'contentQueryStrings' {
|
||||
const strings: IContentQueryStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="mocha" />
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
describe('ContentQueryWebPart', () => {
|
||||
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