This commit is contained in:
VesaJuvonen 2017-10-03 14:59:38 +03:00
commit 80f0c7d70e
44 changed files with 2022 additions and 1636 deletions

View File

@ -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

32
samples/pnp-controls/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.2.0",
"libraryName": "component-test",
"libraryId": "7d4d5b98-cea3-4361-8e3d-5143d5c86334",
"environment": "spo"
}
}

View File

@ -0,0 +1,57 @@
# SharePoint Framework PnP Controls Sample
## Summary
This is a sample project that contains a web part which makes use of the PnP SPFx Controls:
- [SharePoint Framework React Controls](https://www.npmjs.com/package/@pnp/spfx-controls-react)
- [SharePoint Framework Property Controls](https://www.npmjs.com/package/@pnp/spfx-property-controls)
![Web part outcome](./assets/webpart-outcome.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.2.0-green.svg)
## Which PnP SPFx controls are being used in this sample?
The sample makes use of the following controls:
- PropertyFieldListPicker
- PropertyFieldTermPicker
- Placeholder
- ListView (which also uses the FileTypeIcon control)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
## Solution
Solution|Author(s)
--------|---------
pnp-controls|Elio Struyf (MVP, U2U, [@eliostruyf](https://twitter.com/eliostruyf))
## Version history
Version|Date|Comments
-------|----|--------
0.0.1|September 20, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
To test out this web part, you need to have a library with a managed metadata field. In my case, I made use of a field called **Country**.
![Documents](./assets/documents.png)
Once you have such a library in place, you can copy the code and run the following commands:
```bash
npm install
gulp serve --nobrowser
```
![](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/pnp-controls)

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -0,0 +1,15 @@
{
"version": "2.0",
"bundles": {
"pnp-controls-web-part": {
"components": [{
"entrypoint": "./lib/webparts/pnpControls/PnPControlsWebPart.js",
"manifest": "./src/webparts/pnpControls/PnPControlsWebPart.manifest.json"
}]
}
},
"externals": {},
"localizedResources": {
"PnPControlsWebPartStrings": "lib/webparts/pnpControls/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "component-test",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "component-test-client-side-solution",
"id": "7d4d5b98-cea3-4361-8e3d-5143d5c86334",
"version": "1.0.0.0",
"skipFeatureDeployment": false
},
"paths": {
"zippedPackage": "solution/component-test.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,36 @@
{
"name": "component-test",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.2.0",
"@microsoft/sp-webpart-base": "~1.2.0",
"@pnp/spfx-controls-react": "1.0.0-beta.5",
"@pnp/spfx-property-controls": "1.0.0-beta.1",
"@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"moment": "2.18.1",
"react": "15.4.2",
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.2.0",
"@microsoft/sp-module-interfaces": "~1.2.0",
"@microsoft/sp-webpart-workbench": "~1.2.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
}
}

View File

@ -0,0 +1,7 @@
import { ICheckedTerms } from "@pnp/spfx-property-controls/lib/PropertyFieldTermPicker";
export interface IPnPControlsWebPartProps {
lists: string | string[]; // Stores the list ID(s)
terms: ICheckedTerms; // Keeps hold of the selected terms
description: string;
}

View File

@ -0,0 +1,33 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "8909dae1-5562-4f0f-be6c-aa14eac34c66",
"alias": "PnPControlsWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "8909dae1-5562-4f0f-be6c-aa14eac34c66",
"group": {
"default": "Under Development"
},
"title": {
"default": "PnPControls"
},
"description": {
"default": "Web part to test out the PnP SPFx controls"
},
"iconImageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAcCAYAAAATFf3WAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAA6ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNy0wOS0xOVQxMjowOTozOTwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjY8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6Q29tcHJlc3Npb24+NTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+NzI8L3RpZmY6WVJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyPC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NDA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjI4PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CjwcPPMAAAP9SURBVFgJnZfLi49RGMdn3GsUCymXDM0wGCHFkin+ABZYTGTkmmxZoazYSuS2UZNEYiFFJJeFRimxYGE0CwvKXe6X7+c3z1dn3jnv/H6/+db397znnOd5zvd5znnfaRob8hip6cZY+iMLc5igyWaxTZwfdoXsAfG0OEr8JQ4bJMjhd2ESvynibLE9OE+2RWR+hJiCAgFFeq2syIpj2U+ZwCUKQMCCYKvsdHG8mAMFIQA7TvwrGhaGUObTNfuU2jKBDxXhI06D2cwbMs+m+NGx9Fq4az81T3Ec80sR4F+zSCeqRCY/JKAbbEByd4jkFGVaoKYGwEV0a/Z58Gh4kDtXfCzXZhBGohzZ3IKL647rkg8d9ZgYfHeIoOzk+leT37IO5ipkE7rJmuPYNAd8KOJtLH4PuzQscc4RU3lTk5NCEYevK7dQhOREModvkwi8T6eeN4s+Ac/jk0VVB0VZ3Fc97xUXi3z31oqfxJxIjpciekUwRnwg3hPPiOdEQG7i6wbJ6QIJqJbnNWIRfZpgzT6+c75rk7W2SaRzfH7AbpGYEwyE0aJPpjJRy48FesOeCJope1t8LB4SX4k5gds0nwOdBQdF4jpEo65OWuAPRZPoQmRZH2PmTLrsZxe0Pfw5WroDEZBeqV6N34iXRTpcFyzQG96JaC499wdB6fGXCSwenQXyV4o33HHYDWLNsEBEuEMrk+iNev4iktj3j2cX5A4WBXp8OGK/yfLyEXtfHARXNGghJjgWBILz4qrKU0PDWdnV8UwONqgHufuWmyvN6Q6ycZH7kqirse7O2ZZ10C/JQsV9KOTekuSt+miBPt6PhWTLI8ORmPfLVBQ41EvyQrHvxeviVjEL34nsoia5X/jcEHmT94uvxT4RTOo3g3595BZsBzpI0ZzCLJErc1OsG+6gO3Mpk2GG5t6JiKEQrAX5Q00BneI6cawIdon4nmIg0IBqjao4pj8W6I35JOwRF4lzRF6QJyIb2Td99pE9Ch/W7orXYnxR1qj2otpvgE035UjYwHRXU0Fecwe75M9xfo44Pif4ELtTNKqKq+qgTLz+iEQ04G8nnYV+K/U4AMSwzrcyRbcGx0X2heQdEmVnT7UpnNDzjBFRBvwQPzEcvE9PjIl3wTGVNzjm4A5wZCSiG66WtaHEafl/l68wENjnpHiMgUC+muDKis4+Oluv0xnfT0S6kxZs68J5e9sj5pkswMcnUZkY6qdM4DIFzRXTfzunadwk5mLoCJtiKcrd5r4+FQGi8alZHEG5zZjnrvi+MMZvqtgq0hFIAS0i/7i7087nTmqpIgxr0TzXDCcsBrChNyExx9oXvCVr8I98s8i3EdFtYofoO0a3hiVMcRX8A255OkxXy9tuAAAAAElFTkSuQmCC",
"properties": {
"description": "PnPControls",
"fieldName": "Country"
}
}]
}

View File

@ -0,0 +1,90 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'PnPControlsWebPartStrings';
import PnPControls from './components/PnPControls';
import { IPnPControlsProps } from './components/IPnPControlsProps';
import { IPnPControlsWebPartProps } from './IPnPControlsWebPartProps';
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
import { PropertyFieldTermPicker } from '@pnp/spfx-property-controls/lib/PropertyFieldTermPicker';
export default class PnPControlsWebPart extends BaseClientSideWebPart<IPnPControlsWebPartProps> {
public render(): void {
const element: React.ReactElement<IPnPControlsProps> = React.createElement(
PnPControls,
{
context: this.context,
description: this.properties.description,
list: this.properties.lists || "",
terms: this.properties.terms || null
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
PropertyFieldListPicker('lists', {
label: 'Select a list',
selectedList: this.properties.lists,
includeHidden: false,
orderBy: PropertyFieldListPickerOrderBy.Title,
disabled: false,
baseTemplate: 101,
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
properties: this.properties,
context: this.context,
onGetErrorMessage: null,
deferredValidationTime: 0,
key: 'listPickerFieldId'
}),
PropertyFieldTermPicker('terms', {
label: 'Select a term',
panelTitle: 'Select a term',
initialValues: this.properties.terms,
allowMultipleSelections: false,
excludeSystemGroup: false,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
context: this.context,
onGetErrorMessage: null,
deferredValidationTime: 0,
key: 'termSetsPickerFieldId'
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,15 @@
import { ICheckedTerms } from '@pnp/spfx-property-controls/lib/PropertyFieldTermPicker';
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IPnPControlsProps {
context: WebPartContext;
description: string;
list: string | string[];
terms: ICheckedTerms;
}
export interface IPnpControlsState {
items?: any[];
loading?: boolean;
showPlaceholder?: boolean;
}

View File

@ -0,0 +1 @@
.pnpControls {}

View File

@ -0,0 +1,160 @@
import * as React from 'react';
import * as moment from 'moment';
import { IPnPControlsProps, IPnpControlsState } from './IPnPControlsProps';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/components/Spinner';
import { Placeholder } from '@pnp/spfx-controls-react/lib/Placeholder';
import { ListView } from '@pnp/spfx-controls-react/lib/ListView';
import { SPHttpClient } from '@microsoft/sp-http';
import { IViewField } from '@pnp/spfx-controls-react/lib/controls/listView';
export default class PnPControls extends React.Component<IPnPControlsProps, IPnpControlsState> {
// Specify the fields that need to be viewed in the listview
private _viewFields: IViewField[] = [
{
name: "Id",
displayName: "ID",
maxWidth: 25,
minWidth: 25,
sorting: true
},
{
name: "File.Name",
linkPropertyName: "File.ServerRelativeUrl",
displayName: "Name",
sorting: true
},
{
name: "File.TimeCreated",
displayName: "Created",
minWidth: 150,
render: (item: any) => {
const created = item["File.TimeCreated"];
if (created) {
const createdDate = moment(created);
return <span>{createdDate.format('DD/MM/YYYY HH:mm:ss')}</span>;
}
}
}
];
/**
* Constructor
* @param props
*/
constructor(props: IPnPControlsProps) {
super(props);
this.state = {
items: [],
loading: false,
showPlaceholder: (this.props.list === null || this.props.list === "")
};
}
/**
* componentDidMount lifecycle hook
*/
public componentDidMount() {
if (this.props.list !== null && this.props.list !== "") {
this._getListItems();
}
}
/**
* componentDidUpdate lifecycle hook
* @param nextProps
* @param nextState
*/
public componentDidUpdate(prevProps: IPnPControlsProps, prevState: IPnpControlsState) {
if (this.props.list !== prevProps.list || this.props.terms !== prevProps.terms) {
if (this.props.list !== null && this.props.list !== "") {
this._getListItems();
} else {
this.setState({
showPlaceholder: true
});
}
}
}
/**
* Retrieves items for the specified list
* @param listId
*/
private _getListItems() {
this.setState({
loading: true
});
let restApi = `${this.props.context.pageContext.web.absoluteUrl}/_api/web/lists(guid'${this.props.list.toString()}')/items?$expand=File`;
// Check if results need to be filtered
if (typeof this.props.terms !== "undefined" && this.props.terms !== null && this.props.terms.length > 0) {
// Get the first term (single selection)
const term = this.props.terms[0];
// Add the filter to the restApi URL
restApi += `,TaxCatchAll&$select=*,TaxCatchAll/Term&$filter=TaxCatchAll/Term eq '${term.name}'`;
}
this.props.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1)
.then(resp => { return resp.json(); })
.then(items => {
console.log('List Items:', items);
this.setState({
items: items.value ? items.value : [],
loading: false,
showPlaceholder: false
});
});
}
/*
* Opens the web part property pane
*/
private _configureWebPart() {
this.props.context.propertyPane.open();
}
/**
* React render method
*/
public render(): React.ReactElement<IPnPControlsProps> {
// Check if placeholder needs to be shown
if (this.state.showPlaceholder) {
return (
<Placeholder
iconName="Edit"
iconText="List view web part configuration"
description="Please configure the web part before you can show the list view."
buttonLabel="Configure"
onConfigure={this._configureWebPart.bind(this)} />
);
}
return (
<div>
{
this.state.loading ?
(
<Spinner size={SpinnerSize.large} label="Retrieving results ..." />
) : (
this.state.items.length === 0 ?
(
<Placeholder
iconName="InfoSolid"
iconText="No items found"
description="The list or library you selected does not contain items." />
) : (
<div>
<p className="ms-font-xl">{this.props.description}</p>
<ListView items={this.state.items} viewFields={this._viewFields} iconFieldName="File.ServerRelativeUrl" />
</div>
)
)
}
</div>
);
}
}

View File

@ -0,0 +1,7 @@
define([], function () {
return {
"PropertyPaneDescription": "Web Part Configuration",
"BasicGroupName": "PnP Controls",
"DescriptionFieldLabel": "List view title"
}
});

View File

@ -0,0 +1,10 @@
declare interface IPnPControlsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'PnPControlsWebPartStrings' {
const strings: IPnPControlsWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('PnPControlsWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

1
samples/pnp-controls/typings/tsd.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />

View File

@ -29,6 +29,7 @@ Version|Date|Comments
1.0.3|August 12, 2017|Added external scripts functionnality 1.0.3|August 12, 2017|Added external scripts functionnality
1.0.4|August 31, 2017|Fixed a bug where tenant sites/subsites were missing from the **Web Url** dropdown 1.0.4|August 31, 2017|Fixed a bug where tenant sites/subsites were missing from the **Web Url** dropdown
1.0.5|September 1st, 2017|Added a **Site Url** parameter next to the **Web Url** parameter in order to narrow down the results 1.0.5|September 1st, 2017|Added a **Site Url** parameter next to the **Web Url** parameter in order to narrow down the results
1.0.6|September 19, 2017| Upgraded to SharePoint drop 1.2.0 and added the site url and web url preselection when adding the WebPart for the first time on a page. Also fixed a bug with fields that had spaces in their internal names (automatically replaced with `_x0020_` by SharePoint).
## Disclaimer ## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** **THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
@ -37,7 +38,8 @@ Version|Date|Comments
### Cross site collection ### Cross site collection
The WebPart uses the search in order to get all sites under the current domain, which makes it possible to query not only subsites but other site collections and their subsites as well. The WebPart uses the search in order to get all sites under the current domain, which makes it possible to query not only subsites but other site collections and their subsites as well. By default, the current site collection and the current web on which the user is adding the WebPart will be pre-selected automatically.
<img src="Misc/allsites_v2.gif" /> <img src="Misc/allsites_v2.gif" />
<br> <br>

View File

@ -1,13 +1,18 @@
{ {
"entries": [ "$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
{ "version": "2.0",
"entry": "./lib/webparts/contentQuery/ContentQueryWebPart.js", "bundles": {
"manifest": "./src/webparts/contentQuery/ContentQueryWebPart.manifest.json", "content-query-bundle": {
"outputPath": "./dist/content-query.bundle.js" "components": [
{
"entrypoint": "./lib/webparts/contentQuery/ContentQueryWebPart.js",
"manifest": "./src/webparts/contentQuery/ContentQueryWebPart.manifest.json"
}
]
} }
], },
"externals": {},
"localizedResources": { "localizedResources": {
"contentQueryStrings": "webparts/contentQuery/loc/{locale}.js" "contentQueryStrings": "lib/webparts/contentQuery/loc/{locale}.js"
} },
} "externals": {}
}

View File

@ -2,7 +2,7 @@
"solution": { "solution": {
"name": "React Content Query", "name": "React Content Query",
"id": "00406271-0276-406f-9666-512623eb6709", "id": "00406271-0276-406f-9666-512623eb6709",
"version": "1.0.5.0" "version": "1.0.6.0"
}, },
"paths": { "paths": {
"zippedPackage": "solution/react-content-query-webpart.sppkg" "zippedPackage": "solution/react-content-query-webpart.sppkg"

View File

@ -1,2 +1,3 @@
{ {
"cdnBasePath": "https://publiccdn.sharepointonline.com/spptechnologies.sharepoint.com/110700492eeea162ee5bad0f35b1f0061ded8bf436ce0199efe2a4d24109e1c0df1ec594/react-content-query-1.0.6"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
{ {
"name": "react-content-query", "name": "react-content-query",
"version": "1.0.5", "version": "1.0.6",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
}, },
"dependencies": { "dependencies": {
"@microsoft/sp-core-library": "~1.1.0", "@microsoft/sp-core-library": "~1.2.0",
"@microsoft/sp-webpart-base": "~1.1.1", "@microsoft/sp-webpart-base": "~1.2.0",
"@types/handlebars": "4.0.32", "@types/handlebars": "4.0.32",
"@types/react": "0.14.46", "@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17", "@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15", "@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14", "@types/react-addons-update": "0.14.14",
@ -23,9 +23,9 @@
"react-dom": "15.4.2" "react-dom": "15.4.2"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/sp-build-web": "~1.1.0", "@microsoft/sp-build-web": "~1.2.0",
"@microsoft/sp-module-interfaces": "~1.1.0", "@microsoft/sp-module-interfaces": "~1.2.0",
"@microsoft/sp-webpart-workbench": "~1.1.0", "@microsoft/sp-webpart-workbench": "~1.2.0",
"@types/chai": ">=3.4.34 <3.6.0", "@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0", "@types/mocha": ">=2.2.33 <2.6.0",
"awesome-typescript-loader": "^3.2.1", "awesome-typescript-loader": "^3.2.1",

View File

@ -161,7 +161,7 @@ export class CamlQueryHelper {
} }
else if (filter.operator == QueryFilterOperator.ContainsAny || filterUsers == null) 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('') : ''; let values = filterUsers != null ? filterUsers.map(x => Text.format("<Value Type='Integer'>{0}</Value>", x.optionalText)).join('') : '';
filterOutput = Text.format("<In><FieldRef Name='{0}' LookupId='TRUE' /><Values>{1}</Values></In>", filter.field.internalName, values); filterOutput = Text.format("<In><FieldRef Name='{0}' LookupId='TRUE' /><Values>{1}</Values></In>", filter.field.internalName, values);
} }
else if (filter.operator == QueryFilterOperator.ContainsAll) else if (filter.operator == QueryFilterOperator.ContainsAll)
@ -244,8 +244,8 @@ export class CamlQueryHelper {
let digit = parseInt(operatorSplit[operatorSplit.length - 1].replace("[", "").replace("]", "").trim()) * addOrRemove; let digit = parseInt(operatorSplit[operatorSplit.length - 1].replace("[", "").replace("]", "").trim()) * addOrRemove;
let dt = new Date(); let dt = new Date();
dt.setDate(dt.getDate() + digit); dt.setDate(dt.getDate() + digit);
let formattedDate = moment(dt).format("YYYY-MM-DDTHH:mm:ss\\Z"); let formatDate = moment(dt).format("YYYY-MM-DDTHH:mm:ss\\Z");
filterValue = filterValue.replace(result, formattedDate); filterValue = filterValue.replace(result, formatDate);
} }
} }

View File

@ -162,6 +162,10 @@ export class ContentQueryService implements IContentQueryService {
this.searchService.getSitesStartingWith(serverUrl) this.searchService.getSitesStartingWith(serverUrl)
.then((urls) => { .then((urls) => {
// Adds the current site collection url to the ones returned by the search (in case the current site isn't indexed yet)
this.ensureUrl(urls, this.context.pageContext.site.absoluteUrl);
// Builds the IDropdownOption[] based on the urls
let options:IDropdownOption[] = [ { key: "", text: strings.SiteUrlFieldPlaceholder } ]; let options:IDropdownOption[] = [ { key: "", text: strings.SiteUrlFieldPlaceholder } ];
let urlOptions:IDropdownOption[] = urls.sort().map((url) => { let urlOptions:IDropdownOption[] = urls.sort().map((url) => {
let serverRelativeUrl = !isEmpty(url.replace(serverUrl, '')) ? url.replace(serverUrl, '') : '/'; let serverRelativeUrl = !isEmpty(url.replace(serverUrl, '')) ? url.replace(serverUrl, '') : '/';
@ -201,6 +205,12 @@ export class ContentQueryService implements IContentQueryService {
this.searchService.getWebsFromSite(siteUrl) this.searchService.getWebsFromSite(siteUrl)
.then((urls) => { .then((urls) => {
// If querying the current site, adds the current site collection url to the ones returned by the search (in case the current web isn't indexed yet)
if(siteUrl.toLowerCase().trim() === this.context.pageContext.site.absoluteUrl.toLowerCase().trim()) {
this.ensureUrl(urls, this.context.pageContext.web.absoluteUrl);
}
// Builds the IDropdownOption[] based on the urls
let options:IDropdownOption[] = [ { key: "", text: strings.WebUrlFieldPlaceholder } ]; let options:IDropdownOption[] = [ { key: "", text: strings.WebUrlFieldPlaceholder } ];
let urlOptions:IDropdownOption[] = urls.sort().map((url) => { let urlOptions:IDropdownOption[] = urls.sort().map((url) => {
let siteRelativeUrl = !isEmpty(url.replace(siteUrl, '')) ? url.replace(siteUrl, '') : '/'; let siteRelativeUrl = !isEmpty(url.replace(siteUrl, '')) ? url.replace(siteUrl, '') : '/';
@ -372,7 +382,7 @@ export class ContentQueryService implements IContentQueryService {
let users: any[] = JSON.parse(data.value); let users: any[] = JSON.parse(data.value);
let userSuggestions:IPersonaProps[] = users.map((user) => { return { let userSuggestions:IPersonaProps[] = users.map((user) => { return {
primaryText: user.DisplayText, primaryText: user.DisplayText,
value: user.EntityData.SPUserID || user.EntityData.SPGroupID optionalText: user.EntityData.SPUserID || user.EntityData.SPGroupID
}; }); }; });
resolve(this.removeUserSuggestionsDuplicates(userSuggestions, currentPersonas)); resolve(this.removeUserSuggestionsDuplicates(userSuggestions, currentPersonas));
}) })
@ -546,9 +556,11 @@ export class ContentQueryService implements IContentQueryService {
let normalizedResult: any = {}; let normalizedResult: any = {};
for(let viewField of viewFields) { for(let viewField of viewFields) {
let spacesFormattedName = viewField.replace(new RegExp("_x0020_", "g"), "_x005f_x0020_x005f_");
normalizedResult[viewField] = { normalizedResult[viewField] = {
textValue: result.FieldValuesAsText[viewField], textValue: result.FieldValuesAsText[spacesFormattedName],
htmlValue: result.FieldValuesAsHtml[viewField], htmlValue: result.FieldValuesAsHtml[spacesFormattedName],
rawValue: result[viewField] || result[viewField + 'Id'] rawValue: result[viewField] || result[viewField + 'Id']
}; };
} }
@ -626,7 +638,7 @@ export class ContentQueryService implements IContentQueryService {
let trimmedUsers: IPersonaProps[] = []; let trimmedUsers: IPersonaProps[] = [];
for(let user of users) { for(let user of users) {
let isDuplicate = currentUsers.filter((u) => { return u.value === user.value; }).length > 0; let isDuplicate = currentUsers.filter((u) => { return u.optionalText === user.optionalText; }).length > 0;
if(!isDuplicate) { if(!isDuplicate) {
trimmedUsers.push(user); trimmedUsers.push(user);
@ -654,4 +666,19 @@ export class ContentQueryService implements IContentQueryService {
} }
return trimmedTerms; return trimmedTerms;
} }
/***************************************************************************
* Makes sure the specified url is in the given collection, otherwise adds it
* @param urls : An array of urls
* @param urlToEnsure : The url that needs to be ensured
***************************************************************************/
private ensureUrl(urls: string[], urlToEnsure: string) {
urlToEnsure = urlToEnsure.toLowerCase().trim();
let urlExist = urls.filter((u) => { return u.toLowerCase().trim() === urlToEnsure; }).length > 0;
if(!urlExist) {
urls.push(urlToEnsure);
}
}
} }

View File

@ -166,7 +166,7 @@ export class SearchService {
let pathCell = result.Cells.filter((cell) => { return cell.Key == "Path"; })[0]; let pathCell = result.Cells.filter((cell) => { return cell.Key == "Path"; })[0];
pathIndex = result.Cells.indexOf(pathCell); pathIndex = result.Cells.indexOf(pathCell);
} }
urls.push(result.Cells[pathIndex].Value); urls.push(result.Cells[pathIndex].Value.toLowerCase().trim());
} }
return urls; return urls;
} }

View File

@ -40,7 +40,7 @@ export class AsyncChecklist extends React.Component<IAsyncChecklistProps, IAsync
* @param checked : Whether the checkbox is not checked or not * @param checked : Whether the checkbox is not checked or not
*************************************************************************************/ *************************************************************************************/
private onCheckboxChange(ev?: React.FormEvent<HTMLInputElement>, checked?: boolean) { private onCheckboxChange(ev?: React.FormEvent<HTMLInputElement>, checked?: boolean) {
let checkboxKey = ev.currentTarget.attributes.getNamedItem('data').value; let checkboxKey = ev.currentTarget.attributes.getNamedItem('value').value;
let itemIndex = this.checkedItems.indexOf(checkboxKey); let itemIndex = this.checkedItems.indexOf(checkboxKey);
if(checked) { if(checked) {
@ -127,12 +127,12 @@ export class AsyncChecklist extends React.Component<IAsyncChecklistProps, IAsync
const checklistItems = this.state.items.map((item, index) => { const checklistItems = this.state.items.map((item, index) => {
return ( return (
<Checkbox data={ item.id } <Checkbox id={ item.id }
label={ item.label } label={ item.label }
defaultChecked={ this.isCheckboxChecked(item.id) } defaultChecked={ this.isCheckboxChecked(item.id) }
disabled={ this.props.disable } disabled={ this.props.disable }
onChange={ this.onCheckboxChange.bind(this) } onChange={ this.onCheckboxChange.bind(this) }
inputProps={ { data: item.id } } inputProps={ { value: item.id } }
className={ styles.checklistItem } className={ styles.checklistItem }
key={ index } /> key={ index } />
); );

View File

@ -3,9 +3,11 @@ import { Text } from '@microsoft/sp-core-library
import { Dropdown, IDropdownOption, Spinner } from 'office-ui-fabric-react'; import { Dropdown, IDropdownOption, Spinner } from 'office-ui-fabric-react';
import { IAsyncDropdownProps } from './IAsyncDropdownProps'; import { IAsyncDropdownProps } from './IAsyncDropdownProps';
import { IAsyncDropdownState } from './IAsyncDropdownState'; import { IAsyncDropdownState } from './IAsyncDropdownState';
import { cloneDeep } from '@microsoft/sp-lodash-subset';
export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDropdownState> { export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDropdownState> {
/************************************************************************************* /*************************************************************************************
* Component's constructor * Component's constructor
* @param props * @param props
@ -17,6 +19,7 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
this.state = { this.state = {
processed: false, processed: false,
options: new Array<IDropdownOption>(), options: new Array<IDropdownOption>(),
selectedKey: props.selectedKey,
error: null error: null
}; };
} }
@ -47,14 +50,16 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
this.setState({ this.setState({
processed: false, processed: false,
error: null, error: null,
options: new Array<IDropdownOption>() options: new Array<IDropdownOption>(),
selectedKey: null
}); });
this.props.loadOptions().then((options: IDropdownOption[]) => { this.props.loadOptions().then((options: IDropdownOption[]) => {
this.setState({ this.setState({
processed: true, processed: true,
error: null, error: null,
options: options options: options,
selectedKey: this.props.selectedKey
}); });
}) })
.catch((error: any) => { .catch((error: any) => {
@ -67,6 +72,32 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
} }
/*************************************************************************************
* Temporary fix because of an issue introducted in office-ui-fabric-react 4.32.0 :
* https://github.com/OfficeDev/office-ui-fabric-react/issues/2719
* Issue has been resolved but SPFX still refers to 4.32.0, so this is a temporary fix
* while waiting for SPFX to use a more recent version of office-ui-fabric-react
*************************************************************************************/
private onChanged(option: IDropdownOption, index?: number): void {
// reset previously selected options
const options: IDropdownOption[] = this.state.options;
options.forEach((o: IDropdownOption): void => {
if (o.key !== option.key) {
o.selected = false;
}
});
this.setState((prevState: IAsyncDropdownState, props: IAsyncDropdownProps): IAsyncDropdownState => {
prevState.options = options;
prevState.selectedKey = option.key;
return prevState;
});
if (this.props.onChanged) {
this.props.onChanged(option, index);
}
}
/************************************************************************************* /*************************************************************************************
* Renders the the AsyncDropdown component * Renders the the AsyncDropdown component
*************************************************************************************/ *************************************************************************************/
@ -79,8 +110,8 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
<div> <div>
<Dropdown label={this.props.label} <Dropdown label={this.props.label}
isDisabled={this.props.disabled} isDisabled={this.props.disabled}
onChanged={this.props.onChanged} onChanged={this.onChanged.bind(this)}
selectedKey={this.props.selectedKey} selectedKey={this.state.selectedKey}
options={this.state.options} /> options={this.state.options} />
{loading} {loading}

View File

@ -3,5 +3,6 @@ import { IDropdownOption } from 'office-ui-fabric-react';
export interface IAsyncDropdownState { export interface IAsyncDropdownState {
processed: boolean; processed: boolean;
options: IDropdownOption[]; options: IDropdownOption[];
selectedKey: string | number;
error: string; error: string;
} }

View File

@ -19,20 +19,29 @@ $lightgray: #f5f5f5;
} }
} }
div[class~="ms-BasePicker-text"] { :global .ms-BasePicker-text {
background: #fff; background-color: #fff;
}
:global .ms-Checkbox {
margin-top: 10px;
}
:global .ms-Persona-details {
max-width: 165px;
} }
.peoplePicker { .peoplePicker {
&.disabled { &.disabled {
div[class~="ms-PickerPersona-container"] { :global .ms-BasePicker-text {
display: none; background-color: #ccc;
:global .ms-BasePicker-input,
:global .ms-PickerPersona-container {
display: none;
}
} }
} }
} }
span[class~="ms-TagItem-text"] {
max-width: 201px;
}
} }
} }

View File

@ -4,7 +4,7 @@
"id": "46edf08f-95c7-4ca7-9146-6471f9f471be", "id": "46edf08f-95c7-4ca7-9146-6471f9f471be",
"alias": "ContentQueryWebPart", "alias": "ContentQueryWebPart",
"componentType": "WebPart", "componentType": "WebPart",
"version": "1.0.5", "version": "1.0.6",
"manifestVersion": 2, "manifestVersion": 2,
"preconfiguredEntries": [{ "preconfiguredEntries": [{

View File

@ -56,7 +56,7 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
* Returns the WebPart's version * Returns the WebPart's version
***************************************************************************/ ***************************************************************************/
protected get dataVersion(): Version { protected get dataVersion(): Version {
return Version.parse('1.0.5'); return Version.parse('1.0.6');
} }
@ -66,6 +66,8 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
protected onInit(): Promise<void> { protected onInit(): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.ContentQueryService = new ContentQueryService(this.context, this.context.spHttpClient); this.ContentQueryService = new ContentQueryService(this.context, this.context.spHttpClient);
this.properties.siteUrl = this.properties.siteUrl ? this.properties.siteUrl : this.context.pageContext.site.absoluteUrl.toLowerCase().trim();
this.properties.webUrl = this.properties.webUrl ? this.properties.webUrl : this.context.pageContext.web.absoluteUrl.toLocaleLowerCase().trim();
resolve(); resolve();
}); });
} }
@ -413,31 +415,6 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
} }
/***************************************************************************
* Resets dependent property panes if needed
***************************************************************************/
private resetDependentPropertyPanes(propertyPath: string): void {
if(propertyPath == ContentQueryConstants.propertySiteUrl) {
this.resetWebUrlPropertyPane();
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if(propertyPath == ContentQueryConstants.propertyWebUrl) {
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if (propertyPath == ContentQueryConstants.propertyListTitle) {
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
}
/*************************************************************************** /***************************************************************************
* Validates the templateUrl property * Validates the templateUrl property
***************************************************************************/ ***************************************************************************/
@ -483,13 +460,38 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
} }
/***************************************************************************
* Resets dependent property panes if needed
***************************************************************************/
private resetDependentPropertyPanes(propertyPath: string): void {
if(propertyPath == ContentQueryConstants.propertySiteUrl) {
this.resetWebUrlPropertyPane();
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if(propertyPath == ContentQueryConstants.propertyWebUrl) {
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if (propertyPath == ContentQueryConstants.propertyListTitle) {
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
}
/*************************************************************************** /***************************************************************************
* Resets the List Title property pane and re-renders it * Resets the List Title property pane and re-renders it
***************************************************************************/ ***************************************************************************/
private resetWebUrlPropertyPane() { private resetWebUrlPropertyPane() {
Log.verbose(this.logSource, "Resetting 'webUrl' property...", this.context.serviceScope); Log.verbose(this.logSource, "Resetting 'webUrl' property...", this.context.serviceScope);
this.properties.webUrl = null; this.properties.webUrl = "";
this.ContentQueryService.clearCachedWebUrlOptions(); this.ContentQueryService.clearCachedWebUrlOptions();
update(this.properties, ContentQueryConstants.propertyWebUrl, (): any => { return this.properties.webUrl; }); update(this.properties, ContentQueryConstants.propertyWebUrl, (): any => { return this.properties.webUrl; });
this.webUrlDropdown.properties.selectedKey = ""; this.webUrlDropdown.properties.selectedKey = "";

View File

@ -7,10 +7,8 @@ $container-border: 1px solid #dadada;
border: $container-border; border: $container-border;
padding: 20px 20px 15px 20px; padding: 20px 20px 15px 20px;
div[class*='ms-Checkbox'] { :global .ms-Checkbox {
&:first-child { margin-top: 10px;
margin-top: 5px;
}
} }
} }
.cqwpError { .cqwpError {