Added the files to the new repository
This commit is contained in:
commit
2b0d60e069
|
@ -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,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,14 @@
|
|||
# Folders
|
||||
.vscode
|
||||
coverage
|
||||
node_modules
|
||||
sharepoint
|
||||
src
|
||||
temp
|
||||
|
||||
# Files
|
||||
*.csproj
|
||||
.git*
|
||||
.yo-rc.json
|
||||
gulpfile.js
|
||||
tsconfig.json
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"libraryName": "content-query-webpart",
|
||||
"framework": "react",
|
||||
"version": "1.0.0",
|
||||
"libraryId": "00406271-0276-406f-9666-512623eb6709"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
## React Content Query WebPart
|
||||
|
||||
This is where you include your WebPart documentation.
|
||||
|
||||
### 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.
|
||||
|
||||
### Build options
|
||||
|
||||
gulp clean - TODO
|
||||
gulp test - TODO
|
||||
gulp serve - TODO
|
||||
gulp bundle - TODO
|
||||
gulp package-solution - TODO
|
||||
|
||||
|
||||
### TO DOs
|
||||
|
||||
- Add support for URL field types in CAML query
|
|
@ -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,46 @@
|
|||
{
|
||||
// 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": true,
|
||||
"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,
|
||||
"prefer-const": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"cdnBasePath": "https://publiccdn.sharepointonline.com/spptechnologies.sharepoint.com/110700492eeea162ee5bad0f35b1f0061ded8bf436ce0199efe2a4d24109e1c0df1ec594/react-content-query"
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
'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
|
||||
********************************************************************************************/
|
||||
build.configureWebpack.mergeConfig({
|
||||
additionalConfiguration: (generatedConfiguration) => {
|
||||
generatedConfiguration.resolve.alias = { handlebars: 'handlebars/dist/handlebars.min.js' };
|
||||
return generatedConfiguration;
|
||||
}
|
||||
});
|
||||
|
||||
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "react-content-query",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-client-base": "~1.0.0",
|
||||
"@microsoft/sp-core-library": "~1.0.0",
|
||||
"@microsoft/sp-webpart-base": "~1.0.0",
|
||||
"@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",
|
||||
"moment": "^2.18.1",
|
||||
"office-ui-fabric-react": "1.14.3",
|
||||
"react": "15.4.2",
|
||||
"react-dom": "15.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "https://registry.npmjs.org/@microsoft/sp-build-web/-/sp-build-web-1.0.0.tgz",
|
||||
"@microsoft/sp-module-interfaces": "~1.0.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.0.0",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/microsoft-ajax": "0.0.31",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0",
|
||||
"@types/sharepoint": "^2013.1.4",
|
||||
"gulp": "~3.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
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 propertyTemplateUrl = "templateUrl";
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
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)) {
|
||||
query += Text.format('<Where>{0}</Where>', this.generateFilters(querySettings.filters));
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
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(isEmpty(filter.value)) {
|
||||
return '';
|
||||
}
|
||||
else if(filter.me) {
|
||||
filterOutput = Text.format("<In><FieldRef Name='{0}' LookupId='TRUE' /><Values><Value Type='Integer'><UserID /></Value></Values></In>", filter.field.internalName);
|
||||
}
|
||||
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 = {
|
||||
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,563 @@
|
|||
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');
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* 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,24 @@
|
|||
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<IPersonaProps[]>;
|
||||
ensureFileResolves: (filePath: string) => Promise<{}>;
|
||||
isValidTemplateFile: (filePath: string) => boolean;
|
||||
clearCachedListTitleOptions: () => void;
|
||||
clearCachedOrderByOptions: () => void;
|
||||
clearCachedFilterFields: () => void;
|
||||
clearCachedViewFields: () => void;
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
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) };
|
||||
|
||||
// Tests the web URL against 404 errors before executing the query, to avoid a bug that occurs with SPHttpClient.post when trying to post
|
||||
// https://github.com/SharePoint/sp-dev-docs/issues/553
|
||||
this.spHttpClient.get(webUrl, SPHttpClient.configurations.v1, { method: 'HEAD' })
|
||||
.then((headResponse: SPHttpClientResponse) => {
|
||||
if(headResponse.status != 404) {
|
||||
|
||||
// If there is no 404, proceeds with the CAML query
|
||||
this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, options)
|
||||
.then((postResponse: SPHttpClientResponse) => {
|
||||
if(postResponse.ok) {
|
||||
resolve(postResponse.json());
|
||||
}
|
||||
else {
|
||||
reject(postResponse);
|
||||
}
|
||||
})
|
||||
.catch((error) => { reject(error); });
|
||||
}
|
||||
else {
|
||||
reject(headResponse);
|
||||
}
|
||||
})
|
||||
.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,14 @@
|
|||
import { IQueryFilterField } from './IQueryFilterField';
|
||||
import { QueryFilterOperator } from './QueryFilterOperator';
|
||||
import { QueryFilterJoin } from './QueryFilterJoin';
|
||||
import { IPersonaProps, ITag } from 'office-ui-fabric-react';
|
||||
|
||||
export interface IQueryFilter {
|
||||
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,16 @@
|
|||
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, index: number) => void;
|
||||
disabled?: boolean;
|
||||
strings: IQueryFilterStrings;
|
||||
index?: number;
|
||||
}
|
|
@ -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,419 @@
|
|||
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) : { 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 {
|
||||
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, this.props.index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* 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> {
|
||||
|
||||
/*************************************************************************************
|
||||
* Adds a default filter that always appears
|
||||
*************************************************************************************/
|
||||
private defaultFilters:IQueryFilter[] = [
|
||||
{ field: null, operator: QueryFilterOperator.Eq, join: QueryFilterJoin.Or, value: '' }
|
||||
];
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* 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.props.filters;
|
||||
}
|
||||
|
||||
let defaultFilters:IQueryFilter[] = [
|
||||
{ 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.fields = new Array<IQueryFilterField>();
|
||||
prevState.error = null;
|
||||
prevState.filters = this.getDefaultFilters();
|
||||
return prevState;
|
||||
});
|
||||
|
||||
this.props.loadFields().then((fields: IQueryFilterField[]) => {
|
||||
this.setState((prevState: IQueryFilterPanelState, props: IQueryFilterPanelProps): IQueryFilterPanelState => {
|
||||
prevState.loading = false;
|
||||
prevState.fields = fields;
|
||||
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, index:number): 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[index];
|
||||
|
||||
if(this.props.trimEmptyFiltersOnChange && this.isFilterEmpty(oldFilter) && this.isFilterEmpty(filter)) {
|
||||
isWorthNotifyingParent = false;
|
||||
}
|
||||
|
||||
// Updates the modified filter in the state
|
||||
this.state.filters[index] = 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 newFilter:IQueryFilter = { 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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* 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}
|
||||
index={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,23 @@
|
|||
{
|
||||
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||
|
||||
"id": "46edf08f-95c7-4ca7-9146-6471f9f471be",
|
||||
"alias": "ContentQueryWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "0.0.1",
|
||||
"manifestVersion": 2,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "46edf08f-95c7-4ca7-9146-6471f9f471be",
|
||||
"group": { "default": "Under Development" },
|
||||
"title": { "default": "Content by 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": "Un 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": "Content by Query WebPart"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,508 @@
|
|||
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 { IQueryFilter } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilter';
|
||||
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 templateUrlTextField: IPropertyPaneField<IPropertyPaneTextFieldProps>;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Returns the WebPart's version
|
||||
***************************************************************************/
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* 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,
|
||||
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.onWebUrlChange.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.onListTitleChange.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.onOrderByChange.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.onFiltersChange.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.onViewFieldsChange.bind(this),
|
||||
disable: secondCascadingLevelDisabled,
|
||||
strings: strings.viewFieldsChecklistStrings
|
||||
});
|
||||
|
||||
// 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.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);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Handles the change of the webUrl property
|
||||
***************************************************************************/
|
||||
private onWebUrlChange(propertyPath: string, newValue: any): void {
|
||||
Log.verbose(this.logSource, "WebPart property 'webUrl' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
const oldValue = get(this.properties, propertyPath);
|
||||
|
||||
// Stores the new value in web part properties
|
||||
update(this.properties, propertyPath, (): any => { return newValue; });
|
||||
|
||||
// Resets the web-dependent property panes
|
||||
this.resetListTitlePropertyPane();
|
||||
this.resetOrderByPropertyPane();
|
||||
this.resetFiltersPropertyPane();
|
||||
this.resetViewFieldsPropertyPane();
|
||||
|
||||
// Refreshes the web part
|
||||
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Handles the change of the listTitle property
|
||||
***************************************************************************/
|
||||
private onListTitleChange(propertyPath: string, newValue: any): void {
|
||||
Log.verbose(this.logSource, "WebPart property 'listTitle' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
const oldValue = get(this.properties, propertyPath);
|
||||
|
||||
// Stores the new value in web part properties
|
||||
update(this.properties, propertyPath, (): any => { return newValue; });
|
||||
|
||||
// Resets the list-dependent property panes
|
||||
this.resetOrderByPropertyPane();
|
||||
this.resetFiltersPropertyPane();
|
||||
this.resetViewFieldsPropertyPane();
|
||||
|
||||
// refresh web part
|
||||
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Handles the change of the orderBy property
|
||||
***************************************************************************/
|
||||
private onOrderByChange(propertyPath: string, newValue: string): void {
|
||||
Log.verbose(this.logSource, "WebPart property 'orderBy' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
const oldValue = get(this.properties, propertyPath);
|
||||
|
||||
// Stores the new value in web part properties
|
||||
update(this.properties, propertyPath, (): any => { return newValue; });
|
||||
|
||||
// refresh web part
|
||||
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Handles the change of the filters property
|
||||
***************************************************************************/
|
||||
private onFiltersChange(propertyPath: string, newFilters:IQueryFilter[]) {
|
||||
Log.verbose(this.logSource, "WebPart property 'filters' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
const oldValue = get(this.properties, propertyPath);
|
||||
|
||||
// Stores the new value in web part properties
|
||||
update(this.properties, propertyPath, (): any => { return newFilters; });
|
||||
|
||||
// refresh web part
|
||||
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newFilters);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Handles the change of the viewFields property
|
||||
***************************************************************************/
|
||||
private onViewFieldsChange(propertyPath: string, checkedKeys: string[]) {
|
||||
Log.verbose(this.logSource, "WebPart property 'viewFields' has changed, refreshing WebPart...", this.context.serviceScope);
|
||||
const oldValue = get(this.properties, propertyPath);
|
||||
|
||||
// Stores the new value in web part properties
|
||||
update(this.properties, propertyPath, (): any => { return checkedKeys; });
|
||||
|
||||
// refresh web part
|
||||
this.onPropertyPaneFieldChanged(propertyPath, oldValue, checkedKeys);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* 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,13 @@
|
|||
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[];
|
||||
templateUrl: string;
|
||||
}
|
|
@ -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,190 @@
|
|||
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);
|
||||
|
||||
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.props.onLoadTemplate(this.props.templateUrl).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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
||||
/*************************************************************************************
|
||||
* 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}>
|
||||
Configure the following mandatory properties in order to display results :
|
||||
|
||||
<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.TemplateUrlFieldLabel} checked={!isEmpty(this.props.templateUrl)} />
|
||||
</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,13 @@
|
|||
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;
|
||||
templateUrl: string;
|
||||
strings: IContentQueryStrings;
|
||||
stateKey: string;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface IContentQueryState {
|
||||
loading: boolean;
|
||||
processedTemplateResult: string;
|
||||
error: string;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface IContentQueryStrings {
|
||||
loadingItems: 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,90 @@
|
|||
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: "Finally, specify which fields should be available for rendering within the HandleBars template, and specify the URL of the said 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",
|
||||
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}'
|
||||
},
|
||||
contentQueryStrings: {
|
||||
loadingItems: 'Processing query',
|
||||
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}'
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
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;
|
||||
queryFilterPanelStrings: any;
|
||||
viewFieldsChecklistStrings: 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,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"es6-collections",
|
||||
"webpack-env",
|
||||
"microsoft-ajax",
|
||||
"sharepoint"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// 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;
|
|
@ -0,0 +1 @@
|
|||
/// <reference path="@ms/odsp.d.ts" />
|
Loading…
Reference in New Issue