Merge pull request #1667 from pnp/cqwp-mgt

This commit is contained in:
Hugo Bernier 2021-01-07 02:52:21 -05:00 committed by GitHub
commit d3c2436735
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
200 changed files with 5331 additions and 2335 deletions

View File

@ -2,7 +2,7 @@
## Summary
> **NOTE:** This web part was built with SPFx 1.10.0, making it only compatible with SharePoint Online. If you wish, you can use [an earlier version of this web part](../OnPrem/README.md) which is compatible on-premises versions of SharePoint.
> **NOTE:** This web part was built with SPFx 1.11.0, making it only compatible with SharePoint Online. If you wish, you can use [an earlier version of this web part](../react-content-query-onprem/README.md) which is compatible on-premises versions of SharePoint.
The **Content Query web part** is a modern version of the good old **Content by Query web part** that was introduced in SharePoint 2007. Built for Office 365, this modern version is built using the **SharePoint Framework (SPFx)** and uses the latest *Web Stack* practices.
@ -10,9 +10,17 @@ While the original web part was based on an **XSLT** templating engine, this *Re
![Web Part Preview](assets/toolpart.gif)
## Used SharePoint Framework Version
## Compatibility
![1.11.0](https://img.shields.io/badge/drop-1.11.0-green.svg)
![SPFx 1.11](https://img.shields.io/badge/spfx-1.11.0-green.svg)
![Node.js LTS 10.x](https://img.shields.io/badge/Node.js-LTS%2010.x-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-red.svg)
![Teams N/A](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg)
![Workbench Hosted](https://img.shields.io/badge/Workbench-Hosted-yellow.svg)
## Applies to
@ -28,6 +36,7 @@ react-content-query-web part (Online)|Hugo Bernier ([Tahoe Ninjas](http://tahoen
react-content-query-web part (Online)|Paolo Pialorsi ([PiaSys.com](https://piasys.com/), [@PaoloPia](https://twitter.com/PaoloPia?s=20))
react-content-query-web part |Simon-Pierre Plante
react-content-query-web part (Online)|Abderahman Moujahid
## Version history
Version|Date|Comments
@ -48,6 +57,7 @@ Version|Date|Comments
1.0.14|October 30, 2020|Fixed (lookup-)fields with special characters
1.0.15|November 2, 2020|Upgraded to SPFx 1.11; Added support for jsonValue
1.0.16|November 14, 2020|Fixed a bug where the fieldname starts with a special character; Added more special characters
1.1.0|January 5, 2021|Updated dependencies and added MGT support
## Disclaimer
@ -273,16 +283,19 @@ Property | Description
`{{MyField.htmlValue}}` | Renders the HTML value of the field. For example, a *Link* field HTML value would render something like `<a href="...">My Link Field</a>`
`{{MyField.rawValue}}` | Returns the raw value of the field. For example, a *Taxonomy* field raw value would return an object which contains the term `wssId` and its label
`{{MyField.jsonValue}}` | Returns a JSON object value of the field. For example, an *Image* field JSON value would return a JSON object which contains the `serverRelativeUrl` property
`{{MyField.personValue}}` | Returns an object value of a person field. The `personValue` property provides `email`, `displayName` and `image` properties. The `image` property contains `small`, `medium`, and `large` properties, each of which pointing to the profile image URL for the small, medium, and large profile images.
##### Handlebars
```handlebars
{{#each items}}
<div class="item">
<p>MyUserField text value : {{MyUserField.textValue}}</p>
<p>MyUserField html value : {{MyUserField.htmlValue}}</p>
<p>MyUserField raw value : {{MyUserField.rawValue}}</p>
<p>MyField text value : {{MyField.textValue}}</p>
<p>MyField html value : {{MyField.htmlValue}}</p>
<p>MyField raw value : {{MyField.rawValue}}</p>
<p>MyImageField JSON value : {{MyImageField.jsonValue}}</p>
<p>MyPersonField person value : {{MyPersonField.personValue}}</p>
</div>
{{/each}}
```
@ -291,10 +304,11 @@ Property | Description
```html
<div class="item">
<p>MyUserField text value : Simon-Pierre Plante</p>
<p>MyUserField html value : <a href="..." onclick="...">Simon-Pierre Plante</a></p>
<p>MyUserField raw value : 26</p>
<p>MyField text value : Simon-Pierre Plante</p>
<p>MyField html value : <a href="..." onclick="...">Simon-Pierre Plante</a></p>
<p>MyField raw value : 26</p>
<p>MyImageField JSON value: [Object] </p>
<p>MyPersonField person value: [Object] </p>
</div>
...
```
@ -305,6 +319,12 @@ You can use `JSONValue` to parse complex fields -- such as image fields -- and d
<img src="{{MyImageField.jsonValue.serverRelativeUrl}}" />
```
For fields containing person values (e.g.: the `Author`, `Editor` fields and **Person or Group** fields), you can use the `personValue` property to retrieve values such as `email`, `displayName`, and `image.small`, `image.medium`, `image.large`.
```html
<img src="{{MyPersonField.personValue.image.small}}" />
```
### Including your own external scripts and/or block helpers
#### Including basic library files
@ -394,4 +414,61 @@ ReactContentQuery.ExternalScripts.MyCustomBlockHelper = {
}
```
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-web parts/samples/react-content-query-web part/online" />
### Using Microsoft Graph Toolkit
The Content Query Web Part provides support for [Microsoft Graph Toolkit](https://docs.microsoft.com/en-us/graph/toolkit/overview) (MGT) integration to easily query the [Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview) within your handlebars templates.
You can use any of the MGT components without additional steps, although the MGT integration was designed specifically with [Person](https://docs.microsoft.com/en-us/graph/toolkit/components/person), [People](https://docs.microsoft.com/en-us/graph/toolkit/components/people), and [Person card](https://docs.microsoft.com/en-us/graph/toolkit/components/person-card) components.
#### Using MGT with a person field
If you have a SharePoint list containing a user field (like a **Created By** or **Modified By** field) or a person field, you can pass the `email` property from field's the `personValue` to the MGT component's `person-query` attribute.
For example, to use the `mgt-person` component with a person field called `MyPersonField`, you would use the following template:
```handlebars
<mgt-person person-query="{{MyPersonField.personValue.email}}" view="twoLines"></mgt-person>
```
#### Using MGT templates
MGT supports the use of custom templates to modify the content of a components.
According to [MGT documentation](https://docs.microsoft.com/en-us/graph/toolkit/customize-components/templates), the binding syntax to inject dynamic content in a template uses a block delimited by `{{` and `}}` -- which conflicts with the Handlebars binding syntax.
In order to use a custom MGT template in your Handlebars template, use `[[` and `]]` instead of `{{` and `}}` to bind to the MGT context.
For example, you would replace the following Handlebars template:
```handlebars
<mgt-person person-query="{{MyPersonField.personValue.email}}">
<template>
<div data-if="person.image">
<img src="{{ person.image }}" />
</div>
<div data-else>
{{ person.displayName }}
</div>
</template>
</mgt-person>
```
With the following template:
```handlebars
<mgt-person person-query="{{MyPersonField.personValue.email}}">
<template>
<div data-if="person.image">
<img src="[[ person.image ]]" />
</div>
<div data-else>
[[ person.displayName ]]
</div>
</template>
</mgt-person>
```
Note that, in the example above, the `person-query` attribute is still bound using the Handlebars syntax `person-query="{{MyPersonField.personValue.email}}"`, whereas the MGT template uses `[[ person.image ]]` and `[[ person.displayName ]]`.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-web parts/samples/react-content-query-online" />

View File

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 242 KiB

View File

Before

Width:  |  Height:  |  Size: 498 KiB

After

Width:  |  Height:  |  Size: 498 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 716 KiB

After

Width:  |  Height:  |  Size: 716 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

Before

Width:  |  Height:  |  Size: 9.7 MiB

After

Width:  |  Height:  |  Size: 9.7 MiB

View File

@ -0,0 +1,58 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "React Content Query",
"id": "00406271-0276-406f-9666-512623eb6709",
"version": "1.1.0.0",
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read"
},
{
"resource": "Microsoft Graph",
"scope": "People.Read"
},
{
"resource": "Microsoft Graph",
"scope": "Contacts.Read"
},
{
"resource": "Microsoft Graph",
"scope": "User.ReadBasic.All"
},
{
"resource": "Microsoft Graph",
"scope": "Calendars.Read"
},
{
"resource": "Microsoft Graph",
"scope": "Directory.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "User.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Group.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Files.Read.All"
}
],
"includeClientSideAssets": true,
"developer": {
"name": "PnP Community",
"mpnId": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"websiteUrl": ""
}
},
"paths": {
"zippedPackage": "solution/react-content-query-webpart.sppkg"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "react-content-query-webpart",
"version": "1.0.16",
"version": "1.1.0",
"private": true,
"main": "lib/index.js",
"engines": {
@ -12,13 +12,14 @@
"test": "gulp test"
},
"dependencies": {
"@microsoft/mgt": "^2.0.1",
"@microsoft/sp-core-library": "1.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"@pnp/spfx-controls-react": "^1.17.0",
"@pnp/spfx-property-controls": "^1.17.0",
"@pnp/spfx-controls-react": "^2.3.0",
"@pnp/spfx-property-controls": "2.2.0",
"@types/handlebars": "4.0.32",
"acorn": ">=5.7.4",
"atob": ">=2.1.0",
@ -33,7 +34,7 @@
"fstream": ">=1.0.12",
"growl": ">=1.10.0",
"handlebars": "^4.0.6",
"handlebars-helpers": "^0.8.2",
"handlebars-helpers": "^0.8.4",
"hoek": ">=4.2.1",
"is-my-json-valid": ">=2.17.2",
"js-yaml": ">=3.13.1",

View File

@ -0,0 +1,9 @@
import { IPersonValue } from "./IPersonValue";
export interface INormalizedResult {
textValue: string;
htmlValue: string;
rawValue: any;
jsonValue: any;
personValue?: IPersonValue;
}

View File

@ -0,0 +1,9 @@
export interface IPersonValue {
email: string;
displayName: string;
picture: {
small: string;
medium: string;
large: string;
};
}

View File

@ -15,6 +15,8 @@ import { ListService, IListTitle } from './ListService';
import { SearchService } from './SearchService';
import { PeoplePickerService } from './PeoplePickerService';
import { TaxonomyService } from './TaxonomyService';
import { INormalizedResult } from '../dataContracts/INormalizedResult';
import { IPersonValue } from '../dataContracts/IPersonValue';
export class ContentQueryService implements IContentQueryService {
@ -75,7 +77,7 @@ export class ContentQueryService implements IContentQueryService {
return new Promise<IContentQueryTemplateContext>((resolve, reject) => {
// Initializes the base template context
let templateContext: IContentQueryTemplateContext = {
const templateContext: IContentQueryTemplateContext = {
pageContext: this.context.pageContext,
webUrl: querySettings.webUrl,
listId: querySettings.listId,
@ -85,11 +87,12 @@ export class ContentQueryService implements IContentQueryService {
callTimeStamp: callTimeStamp
};
// Builds the CAML query based on the webpart settings
let query = CamlQueryHelper.generateCamlQuery(querySettings);
// Builds the CAML query based on the web part settings
const query: string = 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
// Queries the list with the generated CAML query
this.listService.getListItemsByQuery(querySettings.webUrl, querySettings.listId, query)
.then((data: any) => {
// Updates the template context with the normalized query results
@ -257,7 +260,7 @@ export class ContentQueryService implements IContentQueryService {
return new Promise<IDropdownOption[]>((resolve, reject) => {
this.listService.getListTitlesFromWeb(webUrl).then((listTitles: IListTitle[]) => {
let options: IDropdownOption[] = [{ key: "", text: strings.ListTitleFieldPlaceholder }];
let listTitleOptions = listTitles.map((list) => { return { key: list.id, text: list.title }; });
const listTitleOptions = listTitles.map((list) => { return { key: list.id, text: list.title }; });
options = options.concat(listTitleOptions);
this.listTitleOptions = options;
resolve(options);
@ -290,9 +293,9 @@ export class ContentQueryService implements IContentQueryService {
// Otherwise gets the options asynchronously
return new Promise<IDropdownOption[]>((resolve, reject) => {
this.listService.getListFields(webUrl, listId, ['InternalName', 'Title', 'Sortable'], 'Title').then((data: any) => {
let sortableFields: any[] = data.value.filter((field) => { return field.Sortable == true; });
const 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) }; });
const 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);
@ -480,8 +483,8 @@ export class ContentQueryService implements IContentQueryService {
let selectItemStr = "\n <span><button class='selectItem' data-itemId='{{ID.textValue}}'>Select</button></span>";
let template = Text.format(`<style type="text/css">
.dynamic-template h2 {
font-size: 24px;
font-weight: 300;
font-size: 20px;
font-weight: 600;
color: "[theme:neutralPrimary, default:#323130]";
}
@ -576,31 +579,35 @@ export class ContentQueryService implements IContentQueryService {
* 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[] {
private normalizeQueryResults(results: any[], viewFields: string[]): INormalizedResult[] {
//Log.verbose(this.logSource, "Normalizing results for the requested handlebars context...", this.context.serviceScope);
let normalizedResults: any[] = [];
for (let result of results) {
const normalizedResults: INormalizedResult[] = results.map((result) => {
let normalizedResult: any = {};
let formattedCharsRegex = /_x00(20|3a|[c-f]{1}[0-9a-f]{1})_/gi;
for (let viewField of viewFields) {
//check if the intenal fieldname begins with a special character (_x00)
let viewFieldOdata = viewField;
let viewFieldOdata: string = viewField;
if (viewField.indexOf("_x00") == 0) {
viewFieldOdata = `OData_${viewField}`;
}
let formattedName = viewFieldOdata.replace(formattedCharsRegex, "_x005f_x00$1_x005f_");
formattedName = formattedName.replace(/_x00$/, "_x005f_x00");
const htmlValue: string = result.FieldValuesAsHtml[formattedName];
normalizedResult[viewField] = {
textValue: result.FieldValuesAsText[formattedName],
htmlValue: result.FieldValuesAsHtml[formattedName],
htmlValue: htmlValue,
rawValue: result[viewField] || result[viewField + 'Id'],
jsonValue: this.jsonParseField(result[viewField] || result[viewField + 'Id'])
jsonValue: this.jsonParseField(result[viewField] || result[viewField + 'Id']),
personValue: this.extractPersonInfo(htmlValue)
};
}
normalizedResults.push(normalizedResult);
}
return normalizedResult;
});
return normalizedResults;
}
@ -621,6 +628,50 @@ export class ContentQueryService implements IContentQueryService {
return value;
}
/**
* Returns user profile information based on a user field
* @param htmlValue : A string representation of the HTML field rendering
* This function does a very rudimentary extraction of user information based on very limited
* HTML parsing. We need to update this in the future to make it more sturdy.
*/
private extractPersonInfo(htmlValue: string): IPersonValue {
try {
const sipIndex = htmlValue.indexOf(`sip='`);
if (sipIndex === -1) {
return null;
}
// Try to extract the user email and name
// Get the email address -- we should use RegExp for this, but I suck at RegExp
const sipValue = htmlValue.substring(sipIndex + 5, htmlValue.indexOf(`'`, sipIndex + 5));
const anchorEnd: number = htmlValue.lastIndexOf('</a>');
const anchorStart: number = htmlValue.substring(0, anchorEnd).lastIndexOf('>');
const name: string = htmlValue.substring(anchorStart + 1, anchorEnd);
// Generate picture URLs
const smallPictureUrl: string = `/_layouts/15/userphoto.aspx?size=S&username=${sipValue}`;
const medPictureUrl: string = `/_layouts/15/userphoto.aspx?size=M&username=${sipValue}`;
const largePictureUrl: string = `/_layouts/15/userphoto.aspx?size=L&username=${sipValue}`;
let result: IPersonValue = {
email: sipValue,
displayName: name,
picture: {
small: smallPictureUrl,
medium: medPictureUrl,
large: largePictureUrl
}
};
return result;
} catch (error) {
return null;
}
}
/**************************************************************************************************
* Returns an error message based on the specified error object

View File

@ -0,0 +1,9 @@
export enum QueryFilterFieldType {
Text = 1,
Number = 2,
Datetime = 3,
User = 4,
Lookup = 5,
Taxonomy = 6,
Url = 7
}

View File

@ -19,7 +19,8 @@
},
"officeFabricIconFontName": "QueryList",
"properties": {
"description": "Content Query"
"description": "Content Query",
"enableMGT": false
}
}]
}

View File

@ -1,35 +1,37 @@
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 } from "@microsoft/sp-webpart-base";
import { IPropertyPaneConfiguration, IPropertyPaneField } from "@microsoft/sp-property-pane";
import { IPropertyPaneTextFieldProps, PropertyPaneTextField } from "@microsoft/sp-property-pane";
import { IPropertyPaneChoiceGroupProps, PropertyPaneChoiceGroup } from "@microsoft/sp-property-pane";
import { } from "@microsoft/sp-webpart-base";
import { IPropertyPaneToggleProps, PropertyPaneToggle } from "@microsoft/sp-property-pane";
import { IPropertyPaneLabelProps, PropertyPaneLabel } from "@microsoft/sp-property-pane";
import { IPropertyPaneButtonProps, PropertyPaneButton, PropertyPaneButtonType } from "@microsoft/sp-property-pane";
import { update, get, isEmpty } from '@microsoft/sp-lodash-subset';
import { IDropdownOption, IPersonaProps, ITag } from 'office-ui-fabric-react';
import ContentQuery from './components/ContentQuery';
import { IContentQueryProps } from './components/IContentQueryProps';
import { IQuerySettings } from './components/IQuerySettings';
import { IContentQueryTemplateContext } from './components/IContentQueryTemplateContext';
import { IContentQueryWebPartProps } from './IContentQueryWebPartProps';
import { PropertyPaneAsyncDropdown } from '../../controls/PropertyPaneAsyncDropdown/PropertyPaneAsyncDropdown';
import { PropertyPaneQueryFilterPanel } from '../../controls/PropertyPaneQueryFilterPanel/PropertyPaneQueryFilterPanel';
import { PropertyPaneAsyncChecklist } from '../../controls/PropertyPaneAsyncChecklist/PropertyPaneAsyncChecklist';
import { PropertyPaneTextDialog } from '../../controls/PropertyPaneTextDialog/PropertyPaneTextDialog';
import { IQueryFilterField } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilterField';
import { IChecklistItem } from '../../controls/PropertyPaneAsyncChecklist/components/AsyncChecklist/IChecklistItem';
import { ContentQueryService } from '../../common/services/ContentQueryService';
import { IContentQueryService } from '../../common/services/IContentQueryService';
import { ContentQueryConstants } from '../../common/constants/ContentQueryConstants';
import { IDynamicDataPropertyDefinition } from '@microsoft/sp-dynamic-data';
import { IDynamicDataCallables } from '@microsoft/sp-dynamic-data';
import { IDynamicItem } from '../../common/dataContracts/IDynamicItem';
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
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 } from "@microsoft/sp-webpart-base";
import { IPropertyPaneConfiguration, IPropertyPaneField } from "@microsoft/sp-property-pane";
import { IPropertyPaneTextFieldProps, PropertyPaneTextField } from "@microsoft/sp-property-pane";
import { IPropertyPaneChoiceGroupProps, PropertyPaneChoiceGroup } from "@microsoft/sp-property-pane";
import { } from "@microsoft/sp-webpart-base";
import { IPropertyPaneToggleProps, PropertyPaneToggle } from "@microsoft/sp-property-pane";
import { IPropertyPaneLabelProps, PropertyPaneLabel } from "@microsoft/sp-property-pane";
import { IPropertyPaneButtonProps, PropertyPaneButton, PropertyPaneButtonType } from "@microsoft/sp-property-pane";
import { update, get, isEmpty } from '@microsoft/sp-lodash-subset';
import { IDropdownOption, IPersonaProps, ITag } from 'office-ui-fabric-react';
import ContentQuery from './components/ContentQuery';
import { IContentQueryProps } from './components/IContentQueryProps';
import { IQuerySettings } from './components/IQuerySettings';
import { IContentQueryTemplateContext } from './components/IContentQueryTemplateContext';
import { IContentQueryWebPartProps } from './IContentQueryWebPartProps';
import { PropertyPaneAsyncDropdown } from '../../controls/PropertyPaneAsyncDropdown/PropertyPaneAsyncDropdown';
import { PropertyPaneQueryFilterPanel } from '../../controls/PropertyPaneQueryFilterPanel/PropertyPaneQueryFilterPanel';
import { PropertyPaneAsyncChecklist } from '../../controls/PropertyPaneAsyncChecklist/PropertyPaneAsyncChecklist';
import { PropertyPaneTextDialog } from '../../controls/PropertyPaneTextDialog/PropertyPaneTextDialog';
import { IQueryFilterField } from '../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilterField';
import { IChecklistItem } from '../../controls/PropertyPaneAsyncChecklist/components/AsyncChecklist/IChecklistItem';
import { ContentQueryService } from '../../common/services/ContentQueryService';
import { IContentQueryService } from '../../common/services/IContentQueryService';
import { ContentQueryConstants } from '../../common/constants/ContentQueryConstants';
import { IDynamicDataPropertyDefinition } from '@microsoft/sp-dynamic-data';
import { IDynamicDataCallables } from '@microsoft/sp-dynamic-data';
import { IDynamicItem } from '../../common/dataContracts/IDynamicItem';
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
//import { Providers, SharePointProvider, TemplateHelper } from '@microsoft/mgt';
export default class ContentQueryWebPart
extends BaseClientSideWebPart<IContentQueryWebPartProps>
@ -95,7 +97,7 @@ export default class ContentQueryWebPart
// Register a handler to be notified if the theme variant changes
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
return new Promise<void>((resolve, reject) => {
return new Promise<void>((resolve, _reject) => {
this.ContentQueryService = new ContentQueryService(this.context, this.context.spHttpClient);
this.properties.webUrl = this.properties.siteUrl || this.properties.webUrl ? this.properties.webUrl : this.context.pageContext.web.absoluteUrl.toLocaleLowerCase().trim();
this.properties.siteUrl = this.properties.siteUrl ? this.properties.siteUrl : this.context.pageContext.site.absoluteUrl.toLowerCase().trim();
@ -112,6 +114,7 @@ export default class ContentQueryWebPart
// Select a dummy item
this.selectedItem = { webUrl: '', listId: '', itemId: -1 };
resolve();
});
}
@ -132,6 +135,23 @@ export default class ContentQueryWebPart
viewFields: this.properties.viewFields,
};
// Enable MGT support only if required
if (this.properties.enableMGT)
{
// Add MGT dependencies
const MGT:any = require('@microsoft/mgt');
// We only need to re-register the SharePoint provider if we didn't register it before
if (MGT.Providers.globalProvider === undefined) {
// Register the SharePoint provider
MGT.Providers.globalProvider = new MGT.SharePointProvider(this.context);
}
// Make sure that the custom binding syntax is enabled
// we do this because we don't want the standard MGT template binding ( {{ }} ) to interfere with handlebars syntax
MGT.TemplateHelper.setBindingSyntax('[[', ']]');
}
const element: React.ReactElement<IContentQueryProps> = React.createElement(ContentQuery,
{
onLoadTemplate: this.loadTemplate.bind(this),
@ -303,7 +323,7 @@ export default class ContentQueryWebPart
{
groupName: strings.SourceGroupName,
groupFields: [
PropertyPaneLabel(ContentQueryConstants.propertySiteUrl,{
PropertyPaneLabel(ContentQueryConstants.propertySiteUrl, {
text: strings.SourcePageDescription
}),
this.siteUrlDropdown,
@ -311,15 +331,10 @@ export default class ContentQueryWebPart
this.listTitleDropdown
]
},
// ]
// },
// {
// header: { description: strings.QueryPageDescription },
// groups: [
{
groupName: strings.QueryGroupName,
groupFields: [
PropertyPaneLabel(ContentQueryConstants.propertyOrderBy,{
PropertyPaneLabel(ContentQueryConstants.propertyOrderBy, {
text: strings.QueryPageDescription
}),
this.orderByDropdown,
@ -330,15 +345,10 @@ export default class ContentQueryWebPart
this.filtersPanel
]
},
// ]
// },
// {
// header: { description: strings.DisplayPageDescription },
// groups: [
{
groupName: strings.DisplayGroupName,
groupFields: [
PropertyPaneLabel(ContentQueryConstants.propertyViewFields,{
PropertyPaneLabel(ContentQueryConstants.propertyViewFields, {
text: strings.DisplayPageDescription
}),
this.viewFieldsChecklist,
@ -348,18 +358,19 @@ export default class ContentQueryWebPart
this.templateUrlTextField
]
},
// ]
// },
// {
// header: { description: strings.ExternalPageDescription },
// groups: [
{
groupName: strings.ExternalGroupName,
groupFields: [
PropertyPaneLabel(ContentQueryConstants.propertyExternalScripts,{
PropertyPaneLabel(ContentQueryConstants.propertyExternalScripts, {
text: strings.ExternalPageDescription
}),
this.externalScripts
this.externalScripts,
PropertyPaneToggle("enableMGT", {
label: "Microsoft Graph Toolkit support",
checked: this.properties.enableMGT,
offText: "Disabled",
onText: "Enabled"
})
]
}
]
@ -422,14 +433,14 @@ export default class ContentQueryWebPart
/***************************************************************************
* Loads the HandleBars template from the specified url
***************************************************************************/
private loadTemplate(templateUrl:string): Promise<string> {
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> {
private loadTemplateContext(querySettings: IQuerySettings, callTimeStamp: number): Promise<IContentQueryTemplateContext> {
return this.ContentQueryService.getTemplateContext(querySettings, callTimeStamp);
}
@ -464,14 +475,14 @@ export default class ContentQueryWebPart
/***************************************************************************
* Loads the dropdown options for the listTitle property
***************************************************************************/
private loadFilterFields():Promise<IQueryFilterField[]> {
private loadFilterFields(): Promise<IQueryFilterField[]> {
return this.ContentQueryService.getFilterFields(this.properties.webUrl, this.properties.listId);
}
/***************************************************************************
* Loads the checklist items for the viewFields property
***************************************************************************/
private loadViewFieldsChecklistItems():Promise<IChecklistItem[]> {
private loadViewFieldsChecklistItems(): Promise<IChecklistItem[]> {
return this.ContentQueryService.getViewFieldsChecklistItems(this.properties.webUrl, this.properties.listId);
}
@ -481,7 +492,7 @@ export default class ContentQueryWebPart
* @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[]> {
private loadPeoplePickerSuggestions(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number): Promise<IPersonaProps[]> {
return this.ContentQueryService.getPeoplePickerSuggestions(this.properties.webUrl, filterText, currentPersonas, limitResults);
}
@ -492,7 +503,7 @@ export default class ContentQueryWebPart
* @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[]> {
private loadTaxonomyPickerSuggestions(field: IQueryFilterField, filterText: string, currentTerms: ITag[]): Promise<ITag[]> {
return this.ContentQueryService.getTaxonomyPickerSuggestions(this.properties.webUrl, this.properties.listId, field, filterText, currentTerms);
}
@ -512,7 +523,7 @@ export default class ContentQueryWebPart
this.resetDependentPropertyPanes(propertyPath);
// If the viewfields have changed, update the default template text if it hasn't been altered by the user
if(propertyPath == ContentQueryConstants.propertyViewFields && !this.properties.hasDefaultTemplateBeenUpdated) {
if (propertyPath == ContentQueryConstants.propertyViewFields && !this.properties.hasDefaultTemplateBeenUpdated) {
let generatedTemplate = this.ContentQueryService.generateDefaultTemplate(newValue, this.properties.itemSelectorEnabled);
update(this.properties, ContentQueryConstants.propertyTemplateText, (): any => { return generatedTemplate; });
this.templateTextDialog.properties.dialogTextFieldValue = generatedTemplate;
@ -520,7 +531,7 @@ export default class ContentQueryWebPart
}
// If the templateText have changed, update the "hasDefaultTemplateBeenUpdated" to true so the WebPart doesn't override the user template after updating view fields
if(propertyPath == ContentQueryConstants.propertyTemplateText && !this.properties.hasDefaultTemplateBeenUpdated) {
if (propertyPath == ContentQueryConstants.propertyTemplateText && !this.properties.hasDefaultTemplateBeenUpdated) {
update(this.properties, ContentQueryConstants.propertyhasDefaultTemplateBeenUpdated, (): any => { return true; });
}
@ -529,7 +540,7 @@ export default class ContentQueryWebPart
if (!this.disableReactivePropertyChanges)
this.render();
if(rerenderTemplateTextDialog) {
if (rerenderTemplateTextDialog) {
this.templateTextDialog.render();
}
}
@ -543,21 +554,21 @@ export default class ContentQueryWebPart
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)) {
if (isEmpty(value)) {
resolve('');
}
// Resolves an error if the file isn't a valid .htm or .html file
else if(!this.ContentQueryService.isValidTemplateFile(value)) {
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) => {
this.ContentQueryService.ensureFileResolves(value).then((isFileResolving: boolean) => {
resolve('');
})
.catch((error) => {
resolve(Text.format(strings.ErrorTemplateResolve, error));
});
.catch((error) => {
resolve(Text.format(strings.ErrorTemplateResolve, error));
});
}
});
}
@ -581,14 +592,14 @@ export default class ContentQueryWebPart
* Resets dependent property panes if needed
***************************************************************************/
private resetDependentPropertyPanes(propertyPath: string): void {
if(propertyPath == ContentQueryConstants.propertySiteUrl) {
if (propertyPath == ContentQueryConstants.propertySiteUrl) {
this.resetWebUrlPropertyPane();
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if(propertyPath == ContentQueryConstants.propertyWebUrl) {
else if (propertyPath == ContentQueryConstants.propertyWebUrl) {
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();

View File

@ -1,6 +1,4 @@
import { IQueryFilter } from "../../controls/PropertyPaneQueryFilterPanel/components/QueryFilter/IQueryFilter";
import { IPropertyFieldSite } from "@pnp/spfx-property-controls/lib/PropertyFieldSitePicker";
export interface IContentQueryWebPartProps {
siteUrl: string;
@ -19,4 +17,5 @@ export interface IContentQueryWebPartProps {
externalScripts: string;
itemSelectorEnabled: boolean;
idFieldForciblyAdded: boolean;
enableMGT: boolean;
}

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Some files were not shown because too many files have changed in this diff Show More