Merge branch 'dev'

This commit is contained in:
Vesa Juvonen 2018-12-08 19:03:24 +02:00
commit 7966809468
95 changed files with 36001 additions and 5675 deletions

View File

@ -1,25 +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
# 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

View File

@ -1,32 +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
# 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

@ -1,11 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.6.0",
"libraryName": "dynamic-bundling",
"libraryId": "c40970b0-7ef0-40f2-b03b-718751fbc4b5",
"packageManager": "npm",
"componentType": "webpart"
}
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.7.0",
"libraryName": "dynamic-bundling",
"libraryId": "c40970b0-7ef0-40f2-b03b-718751fbc4b5",
"packageManager": "npm",
"componentType": "webpart",
"isDomainIsolated": false
}
}

View File

@ -1,59 +1,60 @@
# title of the sample
## Summary
This sample illustrates how SPFx functionality and packages can be bundled in multiple '.js' files then be dynamically & asynchronously loaded into the page at execution time, such as with a button click.
Pre Button Click:
![preview](./assets/WebPart-Preview-PreClick.jpg)
Post Button Click that imports jQuery and additional functionality:
![preview](./assets/WebPart-Preview-PostjQueryClick.jpg)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.6.0-blue.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
js-dynamic-bundling-libaries | David Warner II ([@DavidWarnerII](https://twitter.com/davidwarnerii) / [Warner Digital](http://warner.digital))
## Version history
Version|Date|Comments
-------|----|--------
1.0|September 21, 2018|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- How to separate SPFx functionality into multiple bundled files
- How to asynchronously load the seperate bundled files at execution time
- Including a library in the separate bundled file.
## Additional Information:
- [Dynamic loading of packages in SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/dynamic-loading)
- Video Demonstration on using, building and code specifics for the sample web part:<br> http://warner.digital/dynamic-spfx-package-bundling/
# title of the sample
## Summary
This sample illustrates how SPFx functionality and packages can be bundled in multiple '.js' files then be dynamically & asynchronously loaded into the page at execution time, such as with a button click.
Pre Button Click:
![preview](./assets/WebPart-Preview-PreClick.jpg)
Post Button Click that imports jQuery and additional functionality:
![preview](./assets/WebPart-Preview-PostjQueryClick.jpg)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.7.0-orange.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
js-dynamic-bundling-libaries | David Warner II ([@DavidWarnerII](https://twitter.com/davidwarnerii) / [Warner Digital](http://warner.digital))
## Version history
Version|Date|Comments
-------|----|--------
1.0|September 21, 2018|Initial release
1.1|December 3, 2018|Updated for SPFx 1.7.0
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- How to separate SPFx functionality into multiple bundled files
- How to asynchronously load the seperate bundled files at execution time
- Including a library in the separate bundled file.
## Additional Information:
- [Dynamic loading of packages in SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/dynamic-loading)
- Video Demonstration on using, building and code specifics for the sample web part:<br> http://warner.digital/dynamic-spfx-package-bundling/
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />

View File

@ -1,18 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"dynamic-bundling-example-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/dynamicBundlingExample/DynamicBundlingExampleWebPart.js",
"manifest": "./src/webparts/dynamicBundlingExample/DynamicBundlingExampleWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"DynamicBundlingExampleWebPartStrings": "lib/webparts/dynamicBundlingExample/loc/{locale}.js"
}
}
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"dynamic-bundling-example-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/dynamicBundlingExample/DynamicBundlingExampleWebPart.js",
"manifest": "./src/webparts/dynamicBundlingExample/DynamicBundlingExampleWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"DynamicBundlingExampleWebPartStrings": "lib/webparts/dynamicBundlingExample/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -1,12 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "dynamic-bundling-client-side-solution",
"id": "c40970b0-7ef0-40f2-b03b-718751fbc4b5",
"version": "1.0.0.0",
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/dynamic-bundling.sppkg"
}
}
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "dynamic-bundling-client-side-solution",
"id": "c40970b0-7ef0-40f2-b03b-718751fbc4b5",
"version": "1.1.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/dynamic-bundling.sppkg"
}
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -11,23 +11,22 @@
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.6.0",
"@microsoft/sp-lodash-subset": "1.6.0",
"@microsoft/sp-office-ui-fabric-core": "1.6.0",
"@microsoft/sp-webpart-base": "1.6.0",
"@microsoft/sp-core-library": "1.7.0",
"@microsoft/sp-lodash-subset": "1.7.0",
"@microsoft/sp-office-ui-fabric-core": "1.7.0",
"@microsoft/sp-webpart-base": "1.7.0",
"@types/es6-promise": "0.0.33",
"@types/jquery": "^3.3.6",
"@types/webpack-env": "1.13.1",
"jquery": "^3.3.1"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.6.0",
"@microsoft/sp-module-interfaces": "1.6.0",
"@microsoft/sp-webpart-workbench": "1.6.0",
"tslint-microsoft-contrib": "~5.0.0",
"gulp": "~3.9.1",
"@microsoft/sp-build-web": "1.7.0",
"@microsoft/sp-module-interfaces": "1.7.0",
"@microsoft/sp-webpart-workbench": "1.7.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

View File

@ -1 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -1,26 +1,26 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "5378270a-cb87-4be2-b3b7-0ddf597fd77d",
"alias": "DynamicBundlingExampleWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Dynamic Bundling Example" },
"description": { "default": "Dynamic Bundling Example description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "Dynamic Bundling Example"
}
}]
}
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "5378270a-cb87-4be2-b3b7-0ddf597fd77d",
"alias": "DynamicBundlingExampleWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Dynamic Bundling Example" },
"description": { "default": "Dynamic Bundling Example description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "Dynamic Bundling Example"
}
}]
}

View File

@ -1,77 +1,77 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.dynamicBundlingExample {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
width:90% !important;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
width:100% !important;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.dynamicBundlingExample {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
width:90% !important;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
width:100% !important;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -1,101 +1,101 @@
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { escape } from '@microsoft/sp-lodash-subset';
import styles from './DynamicBundlingExampleWebPart.module.scss';
import * as strings from 'DynamicBundlingExampleWebPartStrings';
export interface IDynamicBundlingExampleWebPartProps {
description: string;
}
export default class DynamicBundlingExampleWebPart extends BaseClientSideWebPart<IDynamicBundlingExampleWebPartProps> {
public render(): void {
this.domElement.innerHTML = `
<div class="${ styles.dynamicBundlingExample }">
<div class="${ styles.container }">
<div class="${ styles.row }">
<div class="${ styles.column }">
<span class="${ styles.title }">Dynamic Bundling & Loading of SPFx Packages</span>
<p class="${ styles.subTitle }">When the buttons below are clicked, the associated ".ts" file and it's functionality will be loaded.</p>
<a id="ImportButton" href="#" class="${ styles.button }">
<span class="${ styles.label }">Dynamically Import Functionality</span>
</a>
<a id="ImportButtonJQuery" href="#" class="${ styles.button }">
<span class="${ styles.label }">Dynamically Import Functionality w/ jQuery</span>
</a>
</div>
<div class="insertHTML"></div>
</div>
</div>
</div>`;
this.setupClickEvent();
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
// Create a click event listener for the ImportButton Anchor element
public setupClickEvent(): void {
let btn = document.getElementById("ImportButton");
btn.addEventListener("click", (e:Event) => this.dynamicallyImportFunctions());
let btnjQuery = document.getElementById("ImportButtonJQuery");
btnjQuery.addEventListener("click", (e:Event) => this.dynamicallyImportFunctionsJQuery());
}
// This will setup the 'DynamicImportedFunctions.ts' to be bundled into a separate .js file.
protected async dynamicallyImportFunctions() {
const importedTS = await import(
'./DynamicImportedFunctions'
)
importedTS.default.executeDynamicLoad();
}
//This will setup the 'DynamicImportedFunctionsJQuery.ts' to be bundled into a separate .js file.
protected async dynamicallyImportFunctionsJQuery() {
const importedJQueryTS = await import(
'./DynamicImportedFunctionsJQuery'
)
importedJQueryTS.default.executeDynamicLoadJQuery();
importedJQueryTS.default.addToDOM(this.domElement);
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { escape } from '@microsoft/sp-lodash-subset';
import styles from './DynamicBundlingExampleWebPart.module.scss';
import * as strings from 'DynamicBundlingExampleWebPartStrings';
export interface IDynamicBundlingExampleWebPartProps {
description: string;
}
export default class DynamicBundlingExampleWebPart extends BaseClientSideWebPart<IDynamicBundlingExampleWebPartProps> {
public render(): void {
this.domElement.innerHTML = `
<div class="${ styles.dynamicBundlingExample }">
<div class="${ styles.container }">
<div class="${ styles.row }">
<div class="${ styles.column }">
<span class="${ styles.title }">Dynamic Bundling & Loading of SPFx Packages</span>
<p class="${ styles.subTitle }">When the buttons below are clicked, the associated ".ts" file and it's functionality will be loaded.</p>
<a id="ImportButton" href="#" class="${ styles.button }">
<span class="${ styles.label }">Dynamically Import Functionality</span>
</a>
<a id="ImportButtonJQuery" href="#" class="${ styles.button }">
<span class="${ styles.label }">Dynamically Import Functionality w/ jQuery</span>
</a>
</div>
<div class="insertHTML"></div>
</div>
</div>
</div>`;
this.setupClickEvent();
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
// Create a click event listener for the ImportButton Anchor element
public setupClickEvent(): void {
let btn = document.getElementById("ImportButton");
btn.addEventListener("click", (e:Event) => this.dynamicallyImportFunctions());
let btnjQuery = document.getElementById("ImportButtonJQuery");
btnjQuery.addEventListener("click", (e:Event) => this.dynamicallyImportFunctionsJQuery());
}
// This will setup the 'DynamicImportedFunctions.ts' to be bundled into a separate .js file.
protected async dynamicallyImportFunctions() {
const importedTS = await import(
'./DynamicImportedFunctions'
)
importedTS.default.executeDynamicLoad();
}
//This will setup the 'DynamicImportedFunctionsJQuery.ts' to be bundled into a separate .js file.
protected async dynamicallyImportFunctionsJQuery() {
const importedJQueryTS = await import(
'./DynamicImportedFunctionsJQuery'
)
importedJQueryTS.default.executeDynamicLoadJQuery();
importedJQueryTS.default.addToDOM(this.domElement);
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -1,15 +1,15 @@
/*
This file demonstrates how we can separate functionality that might not need to be loaded with the SPFx Web Part or Extension immediately, but be bundled into a separate .js file and loaded dynamically.
This function will write a message to the console when it is dynamcially loaded.
*/
export default class DynamicFunctions {
public static executeDynamicLoad():void {
console.log('Dynamic Functionality has been imported');
}
}
/*
This file demonstrates how we can separate functionality that might not need to be loaded with the SPFx Web Part or Extension immediately, but be bundled into a separate .js file and loaded dynamically.
This function will write a message to the console when it is dynamcially loaded.
*/
export default class DynamicFunctions {
public static executeDynamicLoad():void {
console.log('Dynamic Functionality has been imported');
}
}

View File

@ -1,6 +1,6 @@
.injectedContent {
clear:both;
padding-top:20px;
margin-left:60px;
font-size:20px;
}
.injectedContent {
clear:both;
padding-top:20px;
margin-left:60px;
font-size:20px;
}

View File

@ -1,32 +1,32 @@
/*
This file demonstrates how we can separate functionality that might not need to be loaded with the SPFx Web Part or Extension immediately, but be bundled into a separate .js file and loaded dynamically.
We will be bundling jQuery in the imported .ts file.
This demonstrates how if we have the need to bundle a third party library, such as jQuery, we can delay the loading of it as well as any additional functionality until aboslutely needed.
This example will add a CSS class to the <body> element, write a message to the console and inject some HTML letting us know that jQuery has been loaded.
*/
import * as $ from 'jquery';
require('./DynamicImportedFunctionsJQuery.scss');
export default class DynamicFunctionsJQuery {
public static executeDynamicLoadJQuery():void {
console.log('Dynamic Functionality with jQuery has been imported');
$('body').addClass('jQueryImported');
}
public static addToDOM(webpart:HTMLElement){
//console.log(webpart);
var item = webpart.getElementsByClassName('insertHTML')[0];
item.innerHTML = `
<div class="injectedContent">
<span class="">jQuery has been dynamically imported.</span>
</div>
`
}
}
/*
This file demonstrates how we can separate functionality that might not need to be loaded with the SPFx Web Part or Extension immediately, but be bundled into a separate .js file and loaded dynamically.
We will be bundling jQuery in the imported .ts file.
This demonstrates how if we have the need to bundle a third party library, such as jQuery, we can delay the loading of it as well as any additional functionality until aboslutely needed.
This example will add a CSS class to the <body> element, write a message to the console and inject some HTML letting us know that jQuery has been loaded.
*/
import * as $ from 'jquery';
require('./DynamicImportedFunctionsJQuery.scss');
export default class DynamicFunctionsJQuery {
public static executeDynamicLoadJQuery():void {
console.log('Dynamic Functionality with jQuery has been imported');
$('body').addClass('jQueryImported');
}
public static addToDOM(webpart:HTMLElement){
//console.log(webpart);
var item = webpart.getElementsByClassName('insertHTML')[0];
item.innerHTML = `
<div class="injectedContent">
<span class="">jQuery has been dynamically imported.</span>
</div>
`
}
}

View File

@ -1,7 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

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

View File

@ -0,0 +1,47 @@
{
"": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.2/MicrosoftTeams.schema.json",
"manifestVersion": "1.2",
"packageName": "Dynamic Bundling Example",
"id": "5378270a-cb87-4be2-b3b7-0ddf597fd77d",
"version": "0.1",
"developer": {
"name": "SPFx + Teams Dev",
"websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration",
"privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement",
"termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement"
},
"name": {
"short": "Dynamic Bundling Example"
},
"description": {
"short": "Dynamic Bundling Example description",
"full": "Dynamic Bundling Example description"
},
"icons": {
"outline": "tab20x20.png",
"color": "tab96x96.png"
},
"accentColor": "#004578",
"configurableTabs": [
{
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=5378270a-cb87-4be2-b3b7-0ddf597fd77d",
"canUpdateConfiguration": true,
"scopes": [
"team"
]
}
],
"validDomains": [
"*.login.microsoftonline.com",
"*.sharepoint.com",
"*.sharepoint-df.com",
"spoppe-a.akamaihd.net",
"spoprod-a.akamaihd.net",
"resourceseng.blob.core.windows.net",
"msft.spoppe.com"
],
"webApplicationInfo": {
"resource": "https://{teamSiteDomain}",
"id": "00000003-0000-0ff1-ce00-000000000000"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,34 +1,34 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -1,32 +1,31 @@
{
"rulesDirectory": [
"tslint-microsoft-contrib"
],
"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-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,
"variable-name": false,
"whitespace": false
}
{
"rulesDirectory": [],
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"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-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,
"variable-name": false,
"whitespace": false
}
}

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

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,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.7.0",
"libraryName": "react-adaptive-cards-image-gallery",
"libraryId": "2321325f-19a4-4895-8bdd-9a5447d462b1",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,81 @@
## Image Gallery Web Part Built with Adaptive Cards
### Summary
This sample demonstrates the capability of using [Adaptive Cards] (https://adaptivecards.io/) with SharePoint Framework. Adaptive cards are great fit for Bot, however they can be effectively used with SPFx to render the content. This web part helps to display the image gallery from SharePoint list.
![Web part preview][figure1]
When added to SharePoint site, the source list containing images information, number of images to display can be configured from web part properties.
The sample also provisions the list called "Adaptive Card Images" which can be used as an example to start using the web part.
![SharePoint Run][figure2]
### SharePoint Asset
A SharePoint list (named "Adaptive Card Images") is provisioned to store the image information. The schema of the list is as below.
![List Schema][figure3]
- The "Image Link" column stores the url of image to be displayed in adaptive card.
- The "Navigation URL" column represents the url to navigate by clicking on image in adaptive card.
- The "Sort Order" column represents the order in which images to be displayed in adaptive card.
The solution also provisions sample data to the "Adaptive Card Images" list.
![List Sample Data][figure4]
### NPM Packages Used
Below NPM packages are used to develop this sample.
1. sp-pnp-js (https://www.npmjs.com/package/sp-pnp-js)
2. adaptivecards (https://www.npmjs.com/package/adaptivecards)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.7-green.svg)
## Applies to
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
react-adaptive-cards-image-gallery|[Nanddeep Nachan](https://www.linkedin.com/in/nanddeepnachan/) (SharePoint Consultant, [@NanddeepNachan](https://http://twitter.com/NanddeepNachan) )
&nbsp;|[Ravi Kulkarni](https://www.linkedin.com/in/ravi-kulkarni-a5381723/) (SharePoint Consultant)
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|November 28, 2018|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.**
---
## Prerequisites
- SharePoint Online tenant
- Site Collection created under the **/sites/** or **/**
## Minimal Path to Awesome
- Clone this repo
- npm i
- gulp serve --nobrowser
- Open workbench on your tennant, i.e. https://contoso.sharepoint.com/sites/salesteam/_layouts/15/workbench.aspx
- Search and add web part "Adaptive Cards Image Gallery"
## Features
This sample web part shows how adaptive cards can be used effectively with SharePoint Framework to render an image gallery with data stored in a SharePoint list.
- Integrating adaptive cards
- Rendering image gallery
- SharePoint assets provisioning
- Creating extensible services
- Using @sp-pnp-js
- Using @adaptivecards
[figure1]: ./assets/webpart-preview.png
[figure2]: ./assets/sharepoint-run.gif
[figure3]: ./assets/list-schema.png
[figure4]: ./assets/list-sample-data.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"adaptive-cards-image-gallery-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/adaptiveCardsImageGallery/AdaptiveCardsImageGalleryWebPart.js",
"manifest": "./src/webparts/adaptiveCardsImageGallery/AdaptiveCardsImageGalleryWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"AdaptiveCardsImageGalleryWebPartStrings": "lib/webparts/adaptiveCardsImageGallery/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-adaptive-cards-image-gallery",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,29 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-adaptive-cards-image-gallery-client-side-solution",
"id": "2321325f-19a4-4895-8bdd-9a5447d462b1",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"features": [
{
"title": "react-adaptive-cards-image-gallery-deployment",
"description": "react-adaptive-cards-image-gallery-deployment",
"id": "bf461e6f-0fd4-4f3c-a77a-bd185e91cf8f",
"version": "1.0.0.0",
"assets": {
"elementManifests": [
"elements.xml"
],
"elementFiles": [
"schema.xml"
]
}
}
]
},
"paths": {
"zippedPackage": "solution/react-adaptive-cards-image-gallery.sppkg"
}
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
{
"name": "react-adaptive-cards-image-gallery",
"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.7.0",
"@microsoft/sp-lodash-subset": "1.7.0",
"@microsoft/sp-office-ui-fabric-core": "1.7.0",
"@microsoft/sp-webpart-base": "1.7.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.4.2",
"@types/react-dom": "16.0.5",
"@types/webpack-env": "1.13.1",
"adaptivecards": "^1.1.0",
"react": "16.3.2",
"react-dom": "16.3.2",
"sp-pnp-js": "^3.0.10"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.7.0",
"@microsoft/sp-tslint-rules": "1.7.0",
"@microsoft/sp-module-interfaces": "1.7.0",
"@microsoft/sp-webpart-workbench": "1.7.0",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<ListInstance
CustomSchema="schema.xml"
FeatureId="00bfea71-de22-43b2-a848-c05709900100"
Title="Adaptive Card Images"
Description="List with image information"
TemplateType="100"
Url="Lists/AdaptiveCardImages">
<Data>
<Rows>
<Row>
<Field Name="Title">Sea</Field>
<Field Name="ImageLink">https://picsum.photos/200/200?image=100, Sea Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Sea, Sea Wikipedia</Field>
<Field Name="SortOrder">1</Field>
</Row>
<Row>
<Field Name="Title">Yak</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=200, Yak Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Domestic_yak, Yak Wikipedia</Field>
<Field Name="SortOrder">2</Field>
</Row>
<Row>
<Field Name="Title">Autumn</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=301, Autumn Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Autumn, Autumn Wikipedia</Field>
<Field Name="SortOrder">3</Field>
</Row>
<Row>
<Field Name="Title">Plant</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=400, Plant Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Plant, Plant Wikipedia</Field>
<Field Name="SortOrder">4</Field>
</Row>
<Row>
<Field Name="Title">Building</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=500, Building Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Building, Building Wikipedia</Field>
<Field Name="SortOrder">5</Field>
</Row>
<Row>
<Field Name="Title">Forest</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=600, Forest Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Forest, Forest Wikipedia</Field>
<Field Name="SortOrder">6</Field>
</Row>
<Row>
<Field Name="Title">Fish</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=700, Fish Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Fish, Fish Wikipedia</Field>
<Field Name="SortOrder">7</Field>
</Row>
<Row>
<Field Name="Title">Auditorium</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=800, Auditorium Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Auditorium, Auditorium Wikipedia</Field>
<Field Name="SortOrder">8</Field>
</Row>
<Row>
<Field Name="Title">Bridge</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=900, Bridge Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Bridge, Bridge Wikipedia</Field>
<Field Name="SortOrder">9</Field>
</Row>
<Row>
<Field Name="Title">Sea Link</Field>
<Field Name="ImageLink">https://picsum.photos/300/200?image=950, Sea Link Image</Field>
<Field Name="NavigationURL">https://en.wikipedia.org/wiki/Sealink, Sea Link Wikipedia</Field>
<Field Name="SortOrder">10</Field>
</Row>
</Rows>
</Data>
</ListInstance>
</Elements>

View File

@ -0,0 +1,33 @@
<List xmlns:ows="Microsoft SharePoint" Title="Image Gallery" EnableContentTypes="TRUE" FolderCreation="FALSE" Direction="$Resources:Direction;" Url="Lists/ImageGallery" BaseType="0" xmlns="http://schemas.microsoft.com/sharepoint/">
<MetaData>
<Fields>
<Field ID="{e6954092-6678-4538-b809-cf9065480a5d}" Type="URL" DisplayName="Image Link" Required="TRUE" Format="Hyperlink" Title="ImageLink" StaticName="ImageLink" Name="ImageLink" ColName="nvarchar4" RowOrdinal="0" ColName2="nvarchar5" RowOrdinal2="0" CustomFormatter="" EnforceUniqueValues="FALSE" Indexed="FALSE" Version="1" />
<Field ID="{3e142196-f3dc-49f1-8147-c7d8b05775d0}" Type="URL" DisplayName="Navigation URL" Required="TRUE" EnforceUniqueValues="FALSE" Indexed="FALSE" Format="Hyperlink" StaticName="NavigationURL" Name="NavigationURL" ColName="nvarchar6" RowOrdinal="0" ColName2="nvarchar7" RowOrdinal2="0" CustomFormatter="" Version="1" />
<Field ID="{8e328744-f48d-4981-8720-d4efa4730f0e}" Type="Number" DisplayName="Sort Order" Required="TRUE" EnforceUniqueValues="FALSE" Indexed="FALSE" StaticName="SortOrder" Name="SortOrder" ColName="float1" RowOrdinal="0" />
</Fields>
<Views>
<View BaseViewID="1" Type="HTML" WebPartZoneID="Main" DisplayName="$Resources:core,objectiv_schema_mwsidcamlidC24;" DefaultView="TRUE" MobileView="TRUE" MobileDefaultView="TRUE" SetupPath="pages\viewpage.aspx" ImageUrl="/_layouts/images/generic.png" Url="AllItems.aspx">
<XslLink Default="TRUE">main.xsl</XslLink>
<JSLink>clienttemplates.js</JSLink>
<RowLimit Paged="TRUE">30</RowLimit>
<Toolbar Type="Standard" />
<ViewFields>
<FieldRef Name="SortOrder"></FieldRef>
<FieldRef Name="LinkTitle"></FieldRef>
<FieldRef Name="ImageLink"></FieldRef>
<FieldRef Name="NavigationURL"></FieldRef>
</ViewFields>
<Query>
<OrderBy>
<FieldRef Name="SortOrder" />
</OrderBy>
</Query>
</View>
</Views>
<Forms>
<Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
<Form Type="EditForm" Url="EditForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
<Form Type="NewForm" Url="NewForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
</Forms>
</MetaData>
</List>

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,26 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "769ec7d3-bd0d-4afc-9426-23a0c279acb4",
"alias": "AdaptiveCardsImageGalleryWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Adaptive Cards Image Gallery" },
"description": { "default": "Image Gallery implemented with Adaptive Cards" },
"officeFabricIconFontName": "ImageDiff",
"properties": {
"description": "AdaptiveCardsImageGallery"
}
}]
}

View File

@ -0,0 +1,74 @@
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 'AdaptiveCardsImageGalleryWebPartStrings';
import AdaptiveCardsImageGallery from './components/AdaptiveCardsImageGallery';
import { IAdaptiveCardsImageGalleryProps } from './components/IAdaptiveCardsImageGalleryProps';
import pnp from 'sp-pnp-js';
export interface IAdaptiveCardsImageGalleryWebPartProps {
imageGalleryName: string;
imagesToDisplay: number;
}
export default class AdaptiveCardsImageGalleryWebPart extends BaseClientSideWebPart<IAdaptiveCardsImageGalleryWebPartProps> {
public async onInit(): Promise<void> {
return super.onInit().then(_ => {
pnp.setup({
spfxContext: this.context
});
});
}
public render(): void {
const element: React.ReactElement<IAdaptiveCardsImageGalleryProps > = React.createElement(
AdaptiveCardsImageGallery,
{
serviceScope: this.context.serviceScope,
imageGalleryName: this.properties.imageGalleryName || "Adaptive Card Images",
imagesToDisplay: this.properties.imagesToDisplay || 10
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('imageGalleryName', {
label: strings.ImageGalleryNameFieldLabel
}),
PropertyPaneTextField('imagesToDisplay', {
label: strings.ImagesToDisplayFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,81 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.adaptiveCardsImageGallery {
.spinner {
display: flex;
justify-content: center;
flex-direction: column;
height: 300px;
}
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,110 @@
import * as React from 'react';
import styles from './AdaptiveCardsImageGallery.module.scss';
import { IAdaptiveCardsImageGalleryProps } from './IAdaptiveCardsImageGalleryProps';
import { IAdaptiveCardsImageGalleryState } from './IAdaptiveCardsImageGalleryState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as AdaptiveCards from "adaptivecards";
import { ImageGalleryService, IImageGalleryService } from '../services/ImageGalleryService';
import { ServiceScope, Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
export default class AdaptiveCardsImageGallery extends React.Component<IAdaptiveCardsImageGalleryProps, IAdaptiveCardsImageGalleryState> {
private ImageGalleryServiceInstance: IImageGalleryService;
private _galleryListName: string;
private _noOfItems: number;
private card: any;
private renderedCard: any = "";
private imagesJSON = [];
constructor(props: IAdaptiveCardsImageGalleryProps) {
super(props);
this.state = {
galleryItems: null,
isLoading: true,
showErrorMessage: false
};
let serviceScope: ServiceScope;
serviceScope = this.props.serviceScope;
this._galleryListName = this.props.imageGalleryName;
this._noOfItems = this.props.imagesToDisplay;
// Based on the type of environment, return the correct instance of the ImageGalleryServiceInstance interface
if (Environment.type == EnvironmentType.SharePoint || Environment.type == EnvironmentType.ClassicSharePoint) {
// Mapping to be used when webpart runs in SharePoint.
this.ImageGalleryServiceInstance = serviceScope.consume(ImageGalleryService.serviceKey);
}
this.ImageGalleryServiceInstance.getGalleryImages(this._galleryListName, this._noOfItems).then((galleryImages: any[]) => {
galleryImages.forEach(adaptiveImage => {
let image = {};
image["type"] = "Image";
image["url"] = adaptiveImage.ImageLink.Url;
// Compose image action
let imageAction = {};
imageAction["title"] = adaptiveImage.NavigationURL.Description;
imageAction["type"] = "Action.OpenUrl";
imageAction["url"] = adaptiveImage.NavigationURL.Url;
imageAction["iconUrl"] = adaptiveImage.NavigationURL.Url;
image["selectAction"] = imageAction;
this.imagesJSON.push(image);
});
this.card = {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Adaptive Image Gallery",
"size": "medium"
},
{
"type": "ImageSet",
"imageSize": "medium",
"images": this.imagesJSON
}
]
};
// Create an AdaptiveCard instance
var adaptiveCard = new AdaptiveCards.AdaptiveCard();
// Set its hostConfig property unless you want to use the default Host Config
// Host Config defines the style and behavior of a card
adaptiveCard.hostConfig = new AdaptiveCards.HostConfig({
fontFamily: "Segoe UI, Helvetica Neue, sans-serif"
});
// Set the adaptive card's event handlers. onExecuteAction is invoked
// whenever an action is clicked in the card
adaptiveCard.onExecuteAction = function(action) {
window.location.href = action.iconUrl;
};
// Parse the card
adaptiveCard.parse(this.card);
// Render the card to an HTML element
this.renderedCard = adaptiveCard.render();
this.setState({ isLoading: false });
});
}
public render(): React.ReactElement<IAdaptiveCardsImageGalleryProps> {
return (
<div className={styles.adaptiveCardsImageGallery}>
<div className={styles.container}>
{this.state.isLoading && <Spinner className={styles.spinner} size={SpinnerSize.large} />}
{!this.state.isLoading && <div ref={(n) => { n && n.appendChild(this.renderedCard) }} />}
</div>
</div>
);
}
}

View File

@ -0,0 +1,7 @@
import { ServiceScope } from '@microsoft/sp-core-library';
export interface IAdaptiveCardsImageGalleryProps {
serviceScope: ServiceScope;
imageGalleryName: string;
imagesToDisplay: number;
}

View File

@ -0,0 +1,5 @@
export interface IAdaptiveCardsImageGalleryState {
galleryItems: any[];
isLoading: boolean;
showErrorMessage: boolean;
}

View File

@ -0,0 +1,9 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"ImageGalleryNameFieldLabel": "Image Gallery",
"ImagesToDisplayFieldLabel": "Number of images to display",
}
});

View File

@ -0,0 +1,11 @@
declare interface IAdaptiveCardsImageGalleryWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
ImageGalleryNameFieldLabel: string;
ImagesToDisplayFieldLabel: string;
}
declare module 'AdaptiveCardsImageGalleryWebPartStrings' {
const strings: IAdaptiveCardsImageGalleryWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,54 @@
import { ServiceKey, ServiceScope } from '@microsoft/sp-core-library';
import { PageContext } from '@microsoft/sp-page-context';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import * as pnp from "sp-pnp-js";
export interface IImageGalleryService {
getGalleryImages: (listName: string, rowLimit: number) => Promise<any[]>;
}
export class ImageGalleryService implements IImageGalleryService {
public static readonly serviceKey: ServiceKey<IImageGalleryService> = ServiceKey.create<IImageGalleryService>('ImageGallery:ImageGalleryService', ImageGalleryService);
private _pageContext: PageContext;
constructor(serviceScope: ServiceScope) {
serviceScope.whenFinished(() => {
this._pageContext = serviceScope.consume(PageContext.serviceKey);
});
}
public getGalleryImages(listName: string, rowLimit: number): Promise<any[]> {
const xml = `<View>
<ViewFields>
<FieldRef Name='ID' />
<FieldRef Name='Title' />
<FieldRef Name='ImageLink' />
<FieldRef Name='NavigationURL' />
</ViewFields>
<Query>
<OrderBy>
<FieldRef Name='SortOrder' />
</OrderBy>
</Query>
<RowLimit>` + rowLimit + `</RowLimit>
</View>`;
const q: any = {
ViewXml: xml,
};
return this._ensureList(listName).then((list) => {
if (list) {
return pnp.sp.web.lists.getByTitle(listName).getItemsByCAMLQuery(q).then((items: any[]) => {
return Promise.resolve(items);
});
}
});
}
private _ensureList(listName: string): Promise<pnp.List> {
if (listName) {
return pnp.sp.web.lists.ensure(listName).then((listEnsureResult) => Promise.resolve(listEnsureResult.list));
}
}
}

View File

@ -0,0 +1,47 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.2/MicrosoftTeams.schema.json",
"manifestVersion": "1.2",
"packageName": "AdaptiveCardsImageGallery",
"id": "769ec7d3-bd0d-4afc-9426-23a0c279acb4",
"version": "0.1",
"developer": {
"name": "SPFx + Teams Dev",
"websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration",
"privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement",
"termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement"
},
"name": {
"short": "AdaptiveCardsImageGallery"
},
"description": {
"short": "Image Gallery implemented with Adaptive Cards",
"full": "Image Gallery implemented with Adaptive Cards"
},
"icons": {
"outline": "tab20x20.png",
"color": "tab96x96.png"
},
"accentColor": "#004578",
"configurableTabs": [
{
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=769ec7d3-bd0d-4afc-9426-23a0c279acb4",
"canUpdateConfiguration": false,
"scopes": [
"team"
]
}
],
"validDomains": [
"*.login.microsoftonline.com",
"*.sharepoint.com",
"*.sharepoint-df.com",
"spoppe-a.akamaihd.net",
"spoprod-a.akamaihd.net",
"resourceseng.blob.core.windows.net",
"msft.spoppe.com"
],
"webApplicationInfo": {
"resource": "https://{teamSiteDomain}",
"id": "00000003-0000-0ff1-ce00-000000000000"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"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-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,
"variable-name": false,
"whitespace": false
}
}

View File

@ -61,6 +61,14 @@ Version|Date|Comments
2.1.1.0 | Oct 30, 2018 | <ul><li>Bug fix for editing custom template.</li><li>Bug fix for dynamic loading of video helper library.</li><li>Added support for Page context query variables.</li><li>Added `getUniqueCount` helper function.</li></ul>
2.1.2.0 | Nov 9, 2018 | <ul><li>Bug fix for IE11.</li><li>Added date query variables.</li><li>Added support for both result source id and query template.</li><li>Added `getUniqueCount` helper function.</li></ul>
2.2.0.0 | Nov 11, 2018 | <ul><li>Upgraded to SPFx 1.7.0</li><li>Added a TypeScript Azure Function to demonstrate NLP processing on search query</li><li>Removed extension data source. Now we use the default SPFx 'Page Environment' data source.</li></ul>
2.2.0.1 | Dec 3, 2018 | <ul><li>Remove switch for handlebar helpers, and instead load helpers if used in the template.</li></ul>
## Important notice on upgrading the solution from pre v2.2.0.0
**Due to code restucturing we have hit an edge case which impacts upgrades from previous versions. To solve the issue go to `https://<tenant>.sharepoint.com/sites/<appcatalog>/Lists/ComponentManifests` and remove the entries for SearchBox and Search Results, and then upload the .sppkg for the new release.**
**Next you need to loop over all sites which have the web parts installed, and upgrade the App on those sites. Now the web parts should work on new and existing sites. You may use the PnP command `Update-PnPApp` to update the application.**
**If you have deployed the solution as a tenant wide extension, this should not impact you.**
## 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.**

View File

@ -3,7 +3,7 @@
"solution": {
"name": "PnP - Search Web Parts",
"id": "890affef-33e0-4d72-bd72-36399e02143b",
"version": "2.2.0.0",
"version": "2.2.0.1",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"isDomainIsolated": false

View File

@ -44,7 +44,5 @@ build.configureWebpack.mergeConfig({
return generatedConfiguration;
}
});
build.webpack.buildConfig
build.addSuppression(new RegExp("\[sass\]",'g'));
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -19,11 +19,13 @@
"@microsoft/sp-lodash-subset": "1.7.0",
"@microsoft/sp-office-ui-fabric-core": "1.7.0",
"@microsoft/sp-webpart-base": "1.7.0",
"@pnp/common": "1.2.5",
"@pnp/logging": "1.2.5",
"@pnp/odata": "1.2.5",
"@pnp/common": "1.2.6",
"@pnp/logging": "1.2.6",
"@pnp/odata": "1.2.6",
"@pnp/polyfill-ie11": "1.0.0",
"@pnp/sp": "1.2.5",
"@pnp/sp": "1.2.6",
"@pnp/sp-taxonomy": "1.2.6",
"@pnp/sp-clientsvc": "1.2.6",
"@pnp/spfx-controls-react": "1.10.0",
"@pnp/spfx-property-controls": "1.12.0",
"@types/es6-promise": "0.0.33",
@ -43,7 +45,7 @@
"on-el-resize": "0.0.4",
"react": "16.3.2",
"react-ace": "6.1.4",
"react-custom-scrollbars": "4.1.2",
"react-custom-scrollbars": "4.2.1",
"react-dom": "16.3.2",
"react-js-pagination": "3.0.0",
"video.js": "^7.3.0"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
interface IRefinerConfiguration {
refinerName: string;
displayValue: string;
}
export default IRefinerConfiguration;

View File

@ -0,0 +1,9 @@
export interface ISortFieldConfiguration {
sortField: string;
sortDirection: ISortFieldDirection;
}
export enum ISortFieldDirection {
Ascending = 1,
Descending= 2
}

View File

@ -0,0 +1,6 @@
interface ISortableFieldConfiguration {
sortField: string;
displayValue: string;
}
export default ISortableFieldConfiguration;

View File

@ -1,4 +1,5 @@
import { ISearchResults, IRefinementFilter } from '../../models/ISearchResult';
import { Sort } from '@pnp/sp';
interface ISearchService {
@ -25,7 +26,7 @@ interface ISearchService {
/**
* The sort order of the results
*/
sortList?: string;
sortList?: Sort[];
/**
* Indicates wheter or not the query rules should be applied in the query

View File

@ -21,7 +21,7 @@ class SearchService implements ISearchService {
private _selectedProperties: string[];
private _queryTemplate: string;
private _resultSourceId: string;
private _sortList: string;
private _sortList: Sort[];
private _enableQueryRules: boolean;
public get resultsCount(): number { return this._resultsCount; }
@ -36,8 +36,8 @@ class SearchService implements ISearchService {
public set resultSourceId(value: string) { this._resultSourceId = value; }
public get resultSourceId(): string { return this._resultSourceId; }
public set sortList(value: string) { this._sortList = value; }
public get sortList(): string { return this._sortList; }
public set sortList(value: Sort[]) { this._sortList = value; }
public get sortList(): Sort[] { return this._sortList; }
public set enableQueryRules(value: boolean) { this._enableQueryRules = value; }
public get enableQueryRules(): boolean { return this._enableQueryRules; }
@ -91,35 +91,7 @@ class SearchService implements ISearchService {
searchQuery.RowLimit = this._resultsCount ? this._resultsCount : 50;
searchQuery.SelectProperties = this._selectedProperties;
searchQuery.TrimDuplicates = false;
let sortList: Sort[] = [
{
Property: 'Created',
Direction: SortDirection.Descending
},
{
Property: 'Size',
Direction: SortDirection.Ascending
}
];
if (this._sortList) {
let sortDirections = this._sortList.split(',');
sortList = sortDirections.map(sorter => {
let sort = sorter.split(':');
let s: Sort = { Property: sort[0].trim(), Direction: SortDirection.Descending };
if (sort.indexOf('[') !== -1) {
s.Direction = SortDirection.FQLFormula;
}
else if (sort.length > 1) {
let direction = sort[1].trim().toLocaleLowerCase();
s.Direction = direction === "ascending" ? SortDirection.Ascending : SortDirection.Descending;
}
return s;
});
}
searchQuery.SortList = sortList;
searchQuery.SortList = this._sortList ? this._sortList : [];
if (refiners) {
// Get the refiners order specified in the property pane
@ -161,8 +133,9 @@ class SearchService implements ISearchService {
const resultRows = r2.RawSearchResults.PrimaryQueryResult.RelevantResults.Table.Rows;
let refinementResultsRows = r2.RawSearchResults.PrimaryQueryResult.RefinementResults;
const refinementRows = refinementResultsRows ? refinementResultsRows['Refiners'] : [];
const refinementRows: any = refinementResultsRows ? refinementResultsRows.Refiners : [];
if (refinementRows.length > 0) {
const component = await import(
/* webpackChunkName: 'search-handlebars-helpers' */
'handlebars-helpers'

View File

@ -1,18 +1,13 @@
interface ITaxonomyService {
import { ITerm } from "@pnp/sp-taxonomy";
/**
* Ensure all script dependencies are loaded before using the taxonomy SharePoint CSOM functions
* https://dev.office.com/sharepoint/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom
* @return {Promise<void>} A promise allowing you to execute your code logic.
*/
initialize();
interface ITaxonomyService {
/**
* Gets multiple terms by their ids using the current taxonomy context
* @param termIds An array of term ids to search for
* @return {Promise<SP.Taxonomy.TermCollection>} A promise containing the terms.
* @return {Promise<ITerm[]>} A promise containing the terms.
*/
getTermsById(termIds: string[]): Promise<SP.Taxonomy.TermCollection>;
getTermsById(termIds: string[]): Promise<ITerm[]>;
}
export default ITaxonomyService;

View File

@ -1,5 +1,6 @@
import ITaxonomyService from './ITaxonomyService';
import { ITerm } from '@pnp/sp-taxonomy';
class MockTaxonomyService implements ITaxonomyService {
@ -11,7 +12,7 @@ class MockTaxonomyService implements ITaxonomyService {
return p1;
}
public getTermsById(termIds: string[]): Promise<SP.Taxonomy.TermCollection> {
public getTermsById(termIds: string[]): Promise<ITerm[]> {
throw new Error('Method not implemented.');
}
}

View File

@ -1,129 +1,38 @@
import { IWebPartContext } from '@microsoft/sp-webpart-base';
import { Logger, LogLevel } from '@pnp/logging';
import { SPComponentLoader } from '@microsoft/sp-loader';
import ITaxonomyService from './ITaxonomyService';
import { Text } from '@microsoft/sp-core-library';
import { ITermStore, ITerms, ITermData, Session, ITerm } from "@pnp/sp-taxonomy";
class TaxonomyService implements ITaxonomyService {
private _workingLanguageLcid: number;
private _context: IWebPartContext;
private _siteUrl: string;
public constructor(webPartContext: IWebPartContext, workingLanguage?: number){
this._context = webPartContext;
this._workingLanguageLcid = workingLanguage ? workingLanguage : null;
}
/**
* Ensure all script dependencies are loaded before using the taxonomy SharePoint CSOM functions
* https://dev.office.com/sharepoint/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom
* @return {Promise<void>} A promise allowing you to execute your code logic.
*/
public initialize(): Promise<void> {
const loadScriptPromise = new Promise<void>((resolve) => {
const siteCollectionUrl = this._context.pageContext.site.absoluteUrl;
SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/init.js', {
globalExportsName: '$_global_init',
})
.catch((error) => {
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "init.js", error));
})
.then((): Promise<{}> => {
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/MicrosoftAjax.js', {
globalExportsName: 'Sys'
});
})
.catch((error) => {
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "MicrosoftAjax.js", error));
})
.then((): Promise<{}> => {
// The SP.Runtime.js file is needed in the hosted workbench environment
// However, in a production environment, there will be an error message in the console saying the file is loaded twice
// This is not a real issue for our purpose so we can keep these lines.
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/SP.Runtime.js', {
globalExportsName: 'SP'
});
})
.catch((error) => {
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "SP.Runtime.js", error));
})
.then((): Promise<{}> => {
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/SP.js', {
globalExportsName: 'SP'
});
})
.catch((error) => {
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "SP.js", error));
})
.then((): Promise<{}> => {
return SPComponentLoader.loadScript(siteCollectionUrl + '/_layouts/15/SP.taxonomy.js', {
globalExportsName: 'SP'
});
})
.catch((error) => {
Logger.write(Text.format("Error when loading '{0}' script. Details: {1}.", "SP.taxonomy.js", error));
})
.then(() => {
// Hack the default method to pass the correct parameters to the server (bug in SP.taxonomy.js)
// https://www.stephensaw.me/sharepoint-sp-taxonomy-js-term-getterms-not-working/
const getTerms: any = function (g, h, e, d, c, f) {
var a = this.get_context(), b;
b = new SP.Taxonomy.TermCollection(a, new SP.ObjectPathMethod(a, this.get_path(), "GetTerms", [g, h, e, d, c, f]));
return b;
};
SP.Taxonomy.Term.prototype.getTerms = getTerms;
resolve();
});
});
return loadScriptPromise;
public constructor(siteUrl: string){
this._siteUrl = siteUrl;
}
/**
* Gets multiple terms by their ids using the current taxonomy context
* @param termIds An array of term ids to search for
*/
public getTermsById(termIds: string[]): Promise<SP.Taxonomy.TermCollection> {
public async getTermsById(termIds: string[]): Promise<(ITerm & ITermData)[]> {
if (termIds.length > 0) {
const spContext = SP.ClientContext.get_current();
const taxSession: SP.Taxonomy.TaxonomySession = SP.Taxonomy.TaxonomySession.getTaxonomySession(spContext);
const termStore = taxSession.getDefaultSiteCollectionTermStore();
if (this._workingLanguageLcid) {
termStore.set_workingLanguage(this._workingLanguageLcid);
}
// The namespace SP is only available here (because of the init() method)
const terms: SP.Taxonomy.TermCollection = termStore.getTermsById(termIds.map(t => new SP.Guid(t)));
// Additional properties can be loaded here
spContext.load(terms, "Include(Id, Name)");
const p = new Promise<SP.Taxonomy.TermCollection>((resolve, reject) => {
spContext.executeQueryAsync(() => {
resolve(terms);
}, (sender, args) => {
const errorMessage = "[TaxonomyProvider.getTermById()]: Error: " + args.get_message();
Logger.write(errorMessage, LogLevel.Error);
reject(errorMessage);
});
const taxonomySession = new Session(this._siteUrl);
taxonomySession.setup({
sp: {
headers: {
Accept: "application/json;odata=nometadata",
},
},
});
return p;
// Get the default termstore
const store: ITermStore = await taxonomySession.getDefaultSiteCollectionTermStore();
const terms: ITerms = await store.getTermsById(...termIds);
return await terms.select('Id','Labels').get();
} else {
return [];
}
}
}

View File

@ -22,16 +22,14 @@ abstract class BaseTemplateService {
this.registerTemplateServices();
}
public async LoadHandlebarsHelpers(load: boolean) {
if (load) {
let component = await import(
/* webpackChunkName: 'search-handlebars-helpers' */
'handlebars-helpers'
);
this._helper = component({
handlebars: Handlebars
});
}
private async LoadHandlebarsHelpers() {
let component = await import(
/* webpackChunkName: 'search-handlebars-helpers' */
'handlebars-helpers'
);
this._helper = component({
handlebars: Handlebars
});
}
/**
@ -292,7 +290,7 @@ abstract class BaseTemplateService {
});
// Return the URL or Title part of a URL automatic managed property
// <p>{{getDate MyLinkOWSURLH "Title"}}</p>
// <p>{{getUrlField MyLinkOWSURLH "Title"}}</p>
Handlebars.registerHelper("getUrlField", (urlField: string, value: "URL" | "Title") => {
let separatorPos = urlField.indexOf(",");
if (value === "URL") {
@ -325,6 +323,168 @@ abstract class BaseTemplateService {
*/
public async processTemplate(templateContext: any, templateContent: string): Promise<string> {
// Process the Handlebars template
const handlebarFunctionNames = [
"getDate",
"after",
"arrayify",
"before",
"eachIndex",
"filter",
"first",
"forEach",
"inArray",
"isArray",
"itemAt",
"join",
"last",
"lengthEqual",
"map",
"some",
"sort",
"sortBy",
"withAfter",
"withBefore",
"withFirst",
"withGroup",
"withLast",
"withSort",
"embed",
"gist",
"jsfiddle",
"isEmpty",
"iterate",
"length",
"and",
"compare",
"contains",
"gt",
"gte",
"has",
"eq",
"ifEven",
"ifNth",
"ifOdd",
"is",
"isnt",
"lt",
"lte",
"neither",
"or",
"unlessEq",
"unlessGt",
"unlessLt",
"unlessGteq",
"unlessLteq",
"moment",
"fileSize",
"read",
"readdir",
"css",
"ellipsis",
"js",
"sanitize",
"truncate",
"ul",
"ol",
"thumbnailImage",
"i18n",
"inflect",
"ordinalize",
"info",
"bold",
"warn",
"error",
"debug",
"_inspect",
"markdown",
"md",
"mm",
"match",
"isMatch",
"add",
"subtract",
"divide",
"multiply",
"floor",
"ceil",
"round",
"sum",
"avg",
"default",
"option",
"noop",
"withHash",
"addCommas",
"phoneNumber",
"random",
"toAbbr",
"toExponential",
"toFixed",
"toFloat",
"toInt",
"toPrecision",
"extend",
"forIn",
"forOwn",
"toPath",
"get",
"getObject",
"hasOwn",
"isObject",
"merge",
"JSONparse",
"parseJSON",
"pick",
"JSONstringify",
"stringify",
"absolute",
"dirname",
"relative",
"basename",
"stem",
"extname",
"segments",
"camelcase",
"capitalize",
"capitalizeAll",
"center",
"chop",
"dashcase",
"dotcase",
"hyphenate",
"isString",
"lowercase",
"occurrences",
"pascalcase",
"pathcase",
"plusify",
"reverse",
"replace",
"sentence",
"snakecase",
"split",
"startsWith",
"titleize",
"trim",
"uppercase",
"encodeURI",
"decodeURI",
"urlResolve",
"urlParse",
"stripQuerystring",
"stripProtocol"
];
for (let i = 0; i < handlebarFunctionNames.length; i++) {
const element = handlebarFunctionNames[i];
let regEx = new RegExp("{{#.*?" + element + ".*?}}", "m");
if (regEx.test(templateContent)) {
await this.LoadHandlebarsHelpers();
break;
}
}
let template = Handlebars.compile(templateContent);
let result = template(templateContext);
if (result.indexOf("-preview-item") != -1) {
@ -418,7 +578,7 @@ abstract class BaseTemplateService {
}));
}
private async _loadVideoLibrary(){
private async _loadVideoLibrary() {
// Load Videos-Js on Demand
// Webpack will create a other bundle loaded on demand just for this library

View File

@ -3,14 +3,9 @@
"id": "096b96cc-8a44-41fa-9b4d-c0ab2ab2a779",
"alias": "SearchBoxWebPart",
"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
"supportsFullBleed": true,
"requiresCustomScript": false,
"preconfiguredEntries": [{

View File

@ -1,5 +1,7 @@
.searchBox {
position: relative;
.errorMessage {
margin-bottom: 10px;
}
@ -12,10 +14,31 @@
padding: 10px;
}
.searchFieldGroup {
display: flex;
position: relative;
.searchTextField {
width: 100%;
}
.searchBtn {
position: absolute;
right: 0;
}
}
.suggestionPanel {
position: absolute;
width: 100%;
box-sizing: border-box;
z-index: 1;
background-color: #ffffff;
border-bottom: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
box-sizing: border-box;
}
.selected {

View File

@ -46,6 +46,8 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
rawInputValue: '',
enhancedQuery: ''
};
this._bindHashChange = this._bindHashChange.bind(this);
}
public render(): void {
@ -53,6 +55,9 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
let inputValue = this.properties.defaultQueryKeywords.tryGetValue();
if (inputValue && typeof(inputValue) === 'string') {
// Notify subsscriber a new value if available
this.context.dynamicDataSourceManager.notifyPropertyChanged('searchQuery');
this._searchQuery.rawInputValue = inputValue;
}
@ -119,6 +124,8 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
this.initSearchService();
this.initNlpService();
this._bindHashChange();
return Promise.resolve();
}
@ -157,6 +164,16 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
protected onPropertyPaneFieldChanged(propertyPath: string) {
this.initSearchService();
this.initNlpService();
if (!this.properties.useDynamicDataSource) {
this.properties.defaultQueryKeywords.setValue("");
} else {
this._bindHashChange();
}
if (propertyPath === 'enableNlpService') {
this.properties.enableDebugMode = !this.properties.enableDebugMode ? false : true;
}
}
/**
@ -352,4 +369,19 @@ export default class SearchBoxWebPart extends BaseClientSideWebPart<ISearchBoxWe
return searchQueryOptimizationFields;
}
/**
* Subscribes to URL hash change if the dynamic property is set to the default 'URL Fragment' property
*/
private _bindHashChange() {
if (this.properties.defaultQueryKeywords.tryGetSource()) {
if (this.properties.defaultQueryKeywords.reference.localeCompare('PageContext:UrlData:fragment') === 0) {
// Manually subscribe to hash change since the default property doesn't
window.addEventListener('hashchange', this.render);
} else {
window.removeEventListener('hashchange', this.render);
}
}
}
}

View File

@ -13,6 +13,7 @@ import * as update from 'immutability-helper';
import styles from '../SearchBoxWebPart.module.scss';
import ISearchQuery from '../../../models/ISearchQuery';
import NlpDebugPanel from './NlpDebugPanel/NlpDebugPanel';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
const SUGGESTION_CHAR_COUNT_TRIGGER = 3;
@ -48,45 +49,49 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
selectedItem,
highlightedIndex,
openMenu,
clearItems
clearItems,
}) => (
<div>
<TextField {...getInputProps({
placeholder: strings.SearchInputPlaceholder,
onKeyDown: event => {
<div className={ styles.searchFieldGroup }>
<TextField {...getInputProps({
placeholder: strings.SearchInputPlaceholder,
onKeyDown: event => {
// Submit search on "Enter"
if (event.keyCode === 13 && (!isOpen || (isOpen && highlightedIndex === null))) {
this._onSearch(this.state.searchInputValue);
// Submit search on "Enter"
if (event.keyCode === 13 && (!isOpen || (isOpen && highlightedIndex === null))) {
this._onSearch(this.state.searchInputValue);
}
}
}
})}
value={ this.state.searchInputValue }
autoComplete= "off"
onChanged={ (value) => {
})}
className={ styles.searchTextField }
value={ this.state.searchInputValue }
autoComplete= "off"
onChanged={ (value) => {
this.setState({
searchInputValue: value,
});
this.setState({
searchInputValue: value,
});
if (this.state.selectedQuerySuggestions.length === 0) {
clearItems();
this._onChange(value);
openMenu();
} else {
if (!value) {
if (this.state.selectedQuerySuggestions.length === 0) {
clearItems();
this._onChange(value);
openMenu();
} else {
if (!value) {
// Reset the selected suggestions if input is empty
this.setState({
selectedQuerySuggestions: [],
});
// Reset the selected suggestions if input is empty
this.setState({
selectedQuerySuggestions: [],
});
}
}
}
}}
iconProps={{
iconName: 'Search',
iconType: IconType.default
}}/>
}}/>
<IconButton iconProps={{
iconName: 'Search',
iconType: IconType.default,
}} onClick= {() => { this._onSearch(this.state.searchInputValue);} } className={ styles.searchBtn }>
</IconButton>
</div>
{isOpen ?
this.renderSuggestions(getItemProps, selectedItem, highlightedIndex)
: null}
@ -96,25 +101,30 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
}
private renderBasicSearchBox(): JSX.Element {
return <TextField
placeholder={ strings.SearchInputPlaceholder }
value={ this.state.searchInputValue }
onChanged={ (value) => {
this.setState({
searchInputValue: value,
});
}}
onKeyDown={ (event) => {
return <div className={ styles.searchFieldGroup }>
<TextField
className={ styles.searchTextField }
placeholder={ strings.SearchInputPlaceholder }
value={ this.state.searchInputValue }
onChanged={ (value) => {
this.setState({
searchInputValue: value,
});
}}
onKeyDown={ (event) => {
// Submit search on "Enter"
if (event.keyCode === 13) {
this._onSearch(this.state.searchInputValue);
}
}}
iconProps={{
iconName: 'Search',
iconType: IconType.default
}}/>;
// Submit search on "Enter"
if (event.keyCode === 13) {
this._onSearch(this.state.searchInputValue);
}
}}
/>
<IconButton iconProps={{
iconName: 'Search',
iconType: IconType.default,
}} onClick= {() => { this._onSearch(this.state.searchInputValue);} } className={ styles.searchBtn }>
</IconButton>
</div>;
}
/**
@ -251,47 +261,52 @@ export default class SearchBoxContainer extends React.Component<ISearchBoxContai
*/
public async _onSearch(queryText: string) {
let query: ISearchQuery = {
rawInputValue: queryText,
enhancedQuery: ''
};
// Don't send empty value
if (queryText) {
this.setState({
searchInputValue: queryText,
});
let query: ISearchQuery = {
rawInputValue: queryText,
enhancedQuery: ''
};
if (this.props.enableNlpService && this.props.NlpService && queryText) {
this.setState({
searchInputValue: queryText,
});
try {
if (this.props.enableNlpService && this.props.NlpService && queryText) {
let enhancedQuery = await this.props.NlpService.enhanceSearchQuery(queryText, this.props.isStaging);
query.enhancedQuery = enhancedQuery.enhancedQuery;
try {
enhancedQuery.entities.map((entity) => {
});
let enhancedQuery = await this.props.NlpService.enhanceSearchQuery(queryText, this.props.isStaging);
query.enhancedQuery = enhancedQuery.enhancedQuery;
this.setState({
enhancedQuery: enhancedQuery,
});
enhancedQuery.entities.map((entity) => {
});
} catch (error) {
// In case of failure, use the non-optimized query instead
query.enhancedQuery = queryText;
this.setState({
enhancedQuery: enhancedQuery,
});
} catch (error) {
// In case of failure, use the non-optimized query instead
query.enhancedQuery = queryText;
}
}
}
if (this.props.searchInNewPage) {
// Send the query to the a new via the query string
const url = UrlHelper.addOrReplaceQueryStringParam(this.props.pageUrl, 'q', encodeURIComponent(queryText));
if (this.props.searchInNewPage) {
// Send the query to the a new via the hash
const url = `${this.props.pageUrl}#${encodeURIComponent(queryText)}`;
const behavior = this.props.openBehavior === PageOpenBehavior.NewTab ? '_blank' : '_self';
window.open(url, behavior);
} else {
const behavior = this.props.openBehavior === PageOpenBehavior.NewTab ? '_blank' : '_self';
window.open(url, behavior);
} else {
// Notify the dynamic data controller
this.props.onSearch(query);
// Notify the dynamic data controller
this.props.onSearch(query);
}
}
}

View File

@ -2,6 +2,7 @@ define([], function() {
return {
"SearchInputPlaceholder": "Entrez vos termes de recherche...",
"SearchBoxNewPage": "Options de la boîte de recherche",
"SearchBoxEnableQuerySuggestions": "Activer les suggestions de recherche",
"SearchBoxSearchInNewPageLabel": "Envoyer la requête sur une nouvelle page",
"SearchBoxPageUrlLabel": "URL de la page",
"SearchBoxUrlErrorMessage": "Veuillez spécifier une URL valide",

View File

@ -1,22 +1,26 @@
import ResultsLayoutOption from '../../models/ResultsLayoutOption';
import { DynamicProperty } from '@microsoft/sp-component-base';
import IRefinerConfiguration from '../../models/IRefinerConfiguration';
import { ISortFieldConfiguration } from '../../models/ISortFieldConfiguration';
import ISortableFieldConfiguration from '../../models/ISortableFieldConfiguration';
export interface ISearchResultsWebPartProps {
queryKeywords: DynamicProperty<string>;
defaultSearchQuery: string;
useDefaultSearchQuery: boolean;
queryTemplate: string;
resultSourceId: string;
sortList: string;
sortList: ISortFieldConfiguration[];
enableQueryRules: boolean;
maxResultsCount: number;
selectedProperties: string;
refiners: string;
sortableFields: string;
refiners: IRefinerConfiguration[];
sortableFields: ISortableFieldConfiguration[];
showPaging: boolean;
showResultsCount: boolean;
showBlank: boolean;
selectedLayout: ResultsLayoutOption;
externalTemplateUrl: string;
inlineTemplateText: string;
useHandlebarsHelpers: boolean;
webPartTitle: string;
}

View File

@ -3,14 +3,9 @@
"id": "42ad2740-3c60-49cf-971a-c44e33511b93",
"alias": "SearchResultsWebPart",
"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
"supportsFullBleed": true,
"requiresCustomScript": false,
"preconfiguredEntries": [
@ -21,23 +16,43 @@
},
"title": {
"default": "Search Results with Refiners",
"fr-fr": "Résultats de recherche"
"fr-fr": "Résultats de recherche"
},
"description": {
"default": "Displays search results with customizable dynamic refiners",
"fr-fr": "Affiche des résulats de recherche avec filtres personnalisables"
"fr-fr": "Affiche des résulats de recherche avec filtres personnalisables"
},
"officeFabricIconFontName": "SearchAndApps",
"properties": {
"queryKeywords": "",
"queryTemplate": "{searchTerms} Path:{Site}",
"refiners": "Created:\"Created Date\",Size:\"Size of the file\"",
"sortList": [
{
"sortField": "Created",
"sortDirection": 1
},
{
"sortField": "Size",
"sortDirection": 2
}
],
"refiners": [
{
"refinerName": "Created",
"displayValue": "Created Date"
},
{
"refinerName": "Size",
"displayValue": "Size of the file"
}
],
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary,FileType,contentclass,ServerRedirectedEmbedURL,DefaultEncodingURL",
"enableQueryRules": false,
"maxResultsCount": 10,
"showBlank": true,
"showResultsCount": true,
"webPartTitle": ""
"webPartTitle": "",
"useDefaultSearchQuery": false
}
}
]

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, Text, Environment, EnvironmentType, DisplayMode, Log } from '@microsoft/sp-core-library';
import {
@ -14,7 +14,8 @@ import {
PropertyPaneToggle,
PropertyPaneSlider,
IPropertyPaneChoiceGroupOption,
PropertyPaneChoiceGroup
PropertyPaneChoiceGroup,
PropertyPaneCheckbox,
} from '@microsoft/sp-webpart-base';
import * as strings from 'SearchResultsWebPartStrings';
import SearchResultsContainer from './components/SearchResultsContainer/SearchResultsContainer';
@ -27,15 +28,16 @@ import TemplateService from '../../services/TemplateService/TemplateService';
import { update, isEmpty } from '@microsoft/sp-lodash-subset';
import MockSearchService from '../../services/SearchService/MockSearchService';
import MockTemplateService from '../../services/TemplateService/MockTemplateService';
import LocalizationHelper from '../../helpers/LocalizationHelper';
import SearchService from '../../services/SearchService/SearchService';
import TaxonomyService from '../../services/TaxonomyService/TaxonomyService';
import MockTaxonomyService from '../../services/TaxonomyService/MockTaxonomyService';
import ISearchResultsContainerProps from './components/SearchResultsContainer/ISearchResultsContainerProps';
import { Placeholder, IPlaceholderProps } from '@pnp/spfx-controls-react/lib/Placeholder';
import { PropertyFieldCollectionData, CustomCollectionFieldType } from '@pnp/spfx-property-controls/lib/PropertyFieldCollectionData';
import { SPHttpClientResponse, SPHttpClient } from '@microsoft/sp-http';
import { SortDirection, Sort } from '@pnp/sp';
import { ISortFieldConfiguration, ISortFieldDirection } from '../../models/ISortFieldConfiguration';
declare var System: any;
const LOG_SOURCE: string = '[SearchResultsWebPart_{0}]';
export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchResultsWebPartProps> {
@ -51,17 +53,12 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
*/
private _templateContentToDisplay: string;
constructor() {
super();
this._parseFieldListString = this._parseFieldListString.bind(this);
}
public async render(): Promise<void> {
// Configure the provider before the query according to our needs
this._searchService.resultsCount = this.properties.maxResultsCount;
this._searchService.queryTemplate = await this.replaceQueryVariables(this.properties.queryTemplate);
this._searchService.resultSourceId = this.properties.resultSourceId;
this._searchService.sortList = this.properties.sortList;
this._searchService.sortList = this._convertToSortList(this.properties.sortList);
this._searchService.enableQueryRules = this.properties.enableQueryRules;
// Determine the template content to display
@ -83,10 +80,9 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
protected renderCompleted(): void {
super.renderCompleted();
let queryKeywords;
let renderElement = null;
if (typeof this.properties.useHandlebarsHelpers === 'undefined') {
this.properties.useHandlebarsHelpers = true;
}
// Get value from data source
const dataSourceValue = this.properties.queryKeywords.tryGetValue();
@ -96,6 +92,12 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
this.context.propertyPane.refresh();
}
if (!dataSourceValue) {
queryKeywords = this.properties.defaultSearchQuery;
} else {
queryKeywords = dataSourceValue;
}
const isValueConnected = !!this.properties.queryKeywords.tryGetSource();
const searchContainer: React.ReactElement<ISearchResultsContainerProps> = React.createElement(
@ -103,14 +105,14 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
{
searchService: this._searchService,
taxonomyService: this._taxonomyService,
queryKeywords: this.properties.queryKeywords.tryGetValue(),
queryKeywords: queryKeywords,
maxResultsCount: this.properties.maxResultsCount,
resultSourceId: this.properties.resultSourceId,
sortList: this.properties.sortList,
sortList: this._convertToSortList(this.properties.sortList),
enableQueryRules: this.properties.enableQueryRules,
selectedProperties: this.properties.selectedProperties ? this.properties.selectedProperties.replace(/\s|,+$/g, '').split(',') : [],
refiners: this._parseFieldListString(this.properties.refiners),
sortableFields: this._parseFieldListString(this.properties.sortableFields),
refiners: this.properties.refiners,
sortableFields: this.properties.sortableFields,
showPaging: this.properties.showPaging,
showResultsCount: this.properties.showResultsCount,
showBlank: this.properties.showBlank,
@ -133,7 +135,9 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
}
);
if (isValueConnected || (!isValueConnected && !isEmpty(this.properties.queryKeywords.tryGetValue()))) {
if (isValueConnected && !this.properties.useDefaultSearchQuery ||
isValueConnected && this.properties.useDefaultSearchQuery && this.properties.defaultSearchQuery ||
!isValueConnected && !isEmpty(queryKeywords)) {
renderElement = searchContainer;
} else {
if (this.displayMode === DisplayMode.Edit) {
@ -148,6 +152,8 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
protected async onInit(): Promise<void> {
this.initializeRequiredProperties();
if (Environment.type === EnvironmentType.Local) {
this._searchService = new MockSearchService();
this._taxonomyService = new MockTaxonomyService();
@ -155,15 +161,11 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
} else {
const lcid = LocalizationHelper.getLocaleId(this.context.pageContext.cultureInfo.currentUICultureName);
this._searchService = new SearchService(this.context);
this._taxonomyService = new TaxonomyService(this.context, lcid);
this._taxonomyService = new TaxonomyService(this.context.pageContext.site.absoluteUrl);
this._templateService = new TemplateService(this.context.spHttpClient, this.context.pageContext.cultureInfo.currentUICultureName);
}
await this._templateService.LoadHandlebarsHelpers(this.properties.useHandlebarsHelpers);
// Configure search query settings
this._useResultSource = false;
@ -173,6 +175,32 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
return super.onInit();
}
private _convertToSortList(sortList: ISortFieldConfiguration[]): Sort[] {
return sortList.map(e => {
let direction;
switch (e.sortDirection) {
case ISortFieldDirection.Ascending:
direction = SortDirection.Ascending;
break;
case ISortFieldDirection.Descending:
direction = SortDirection.Descending;
break;
default:
direction = SortDirection.Ascending;
break;
}
return {
Property: e.sortField,
Direction: direction
} as Sort;
});
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
@ -181,6 +209,37 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
return Version.parse('1.0');
}
/**
* Initializes the Web Part required properties if there are not present in the manifest (i.e. during an update scenario)
*/
private initializeRequiredProperties() {
this.properties.queryTemplate = this.properties.queryTemplate ? this.properties.queryTemplate : "{searchTerms} Path:{Site}";
this.properties.refiners = Array.isArray(this.properties.refiners) ? this.properties.refiners : [
{
refinerName: "Created",
displayValue: "Created Date"
},
{
refinerName: "Size",
displayValue: "Size of the file"
}
];
this.properties.sortList = Array.isArray(this.properties.sortList) ? this.properties.sortList : [
{
sortField: "Created",
sortDirection: ISortFieldDirection.Ascending
},
{
sortField: "Size",
sortDirection: ISortFieldDirection.Descending
}
];
this.properties.sortableFields = Array.isArray(this.properties.sortableFields) ? this.properties.sortableFields : [];
this.properties.selectedProperties = this.properties.selectedProperties ? this.properties.selectedProperties : "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary,FileType,contentclass,ServerRedirectedEmbedURL,DefaultEncodingURL";
this.properties.maxResultsCount = this.properties.maxResultsCount ? this.properties.maxResultsCount : 10;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
@ -234,6 +293,10 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
protected async onPropertyPaneFieldChanged(propertyPath: string) {
if (!this.properties.useDefaultSearchQuery) {
this.properties.defaultSearchQuery = '';
}
if (propertyPath === 'selectedLayout') {
// Refresh setting the right template for the property pane
await this._getTemplateContent();
@ -251,8 +314,6 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
this.properties.externalTemplateUrl = '';
}
}
await this._templateService.LoadHandlebarsHelpers(this.properties.useHandlebarsHelpers);
}
/**
@ -294,39 +355,6 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
return '';
}
/**
* Parses a list of Fields from the property pane value by extracting the managed property and its label.
* @param rawValue the raw value of the refiner
*/
private _parseFieldListString(rawValue: string): { [key: string]: string } {
let returnValues = {};
if(!rawValue) { return returnValues; }
// Get each configuration
let refinerKeyValuePair = rawValue.split(',');
if (refinerKeyValuePair.length > 0) {
refinerKeyValuePair.map((e) => {
const refinerValues = e.split(':');
switch (refinerValues.length) {
case 1:
// Take the same name as the refiner managed property
returnValues[refinerValues[0]] = refinerValues[0];
break;
case 2:
// Trim quotes if present
returnValues[refinerValues[0]] = refinerValues[1].replace(/^'(.*)'$/, '$1');
break;
}
});
}
return returnValues;
}
/**
* Get the correct results template content according to the property pane current configuration
* @returns the template content as a string
@ -489,21 +517,60 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
onGetErrorMessage: this.validateSourceId.bind(this),
deferredValidationTime: 300
}),
PropertyPaneTextField('sortList', {
label: strings.Sort.SortList,
description: strings.Sort.SortListDescription,
multiline: false,
resizable: true,
PropertyFieldCollectionData('sortList', {
manageBtnLabel: strings.ConfigureBtnLabel,
key: 'sortList',
panelHeader: strings.Sort.SortPropertyPaneFieldLabel,
panelDescription: strings.Sort.SortListDescription,
label: strings.Sort.SortPropertyPaneFieldLabel,
value: this.properties.sortList,
deferredValidationTime: 300
fields: [
{
id: 'sortField',
title: "Field name",
type: CustomCollectionFieldType.string,
required: true,
placeholder: '\"Created\", \"Size\", etc.'
},
{
id: 'sortDirection',
title: "Direction",
type: CustomCollectionFieldType.dropdown,
required: true,
options: [
{
key: ISortFieldDirection.Ascending,
text: strings.Sort.SortDirectionAscendingLabel
},
{
key: ISortFieldDirection.Descending,
text: strings.Sort.SortDirectionDescendingLabel
}
]
}
]
}),
PropertyPaneTextField('sortableFields', {
label: strings.SortableFieldsLabel,
description: strings.SortableFieldsDescription,
multiline: true,
resizable: true,
PropertyFieldCollectionData('sortableFields', {
manageBtnLabel: strings.ConfigureBtnLabel,
key: 'sortableFields',
panelHeader: strings.Sort.SortableFieldsPropertyPaneField,
panelDescription: strings.Sort.SortableFieldsDescription,
label: strings.Sort.SortableFieldsPropertyPaneField,
value: this.properties.sortableFields,
deferredValidationTime: 300,
fields: [
{
id: 'sortField',
title: strings.Sort.SortableFieldManagedPropertyField,
type: CustomCollectionFieldType.string,
placeholder: '\"Created\", \"Size\", etc.',
required: true
},
{
id: 'displayValue',
title: strings.Sort.SortableFieldDisplayValueField,
type: CustomCollectionFieldType.string
}
]
}),
PropertyPaneToggle('enableQueryRules', {
label: strings.EnableQueryRulesLabel,
@ -517,13 +584,26 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
value: this.properties.selectedProperties,
deferredValidationTime: 300
}),
PropertyPaneTextField('refiners', {
label: strings.RefinersFieldLabel,
description: strings.RefinersFieldDescription,
multiline: true,
resizable: true,
PropertyFieldCollectionData('refiners', {
manageBtnLabel: strings.ConfigureBtnLabel,
key: 'refiners',
panelHeader: strings.Refiners.RefinersFieldLabel,
panelDescription: strings.Refiners.RefinersFieldDescription,
label: strings.Refiners.RefinersFieldLabel,
value: this.properties.refiners,
deferredValidationTime: 300,
fields: [
{
id: 'refinerName',
title: strings.Refiners.RefinerManagedPropertyField,
type: CustomCollectionFieldType.string,
placeholder: '\"RefinableStringXXX\", etc.'
},
{
id: 'displayValue',
title: strings.Refiners.RefinerDisplayValueField,
type: CustomCollectionFieldType.string
}
]
}),
PropertyPaneSlider('maxResultsCount', {
label: strings.MaxResultsCount,
@ -543,6 +623,30 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
*/
private _getSearchQueryFields(): IPropertyPaneConditionalGroup {
let defaultSearchQueryFields: IPropertyPaneField<any>[] = [];
if (!!this.properties.queryKeywords.tryGetSource()) {
defaultSearchQueryFields.push(
PropertyPaneCheckbox('useDefaultSearchQuery', {
text: strings.UseDefaultSearchQueryKeywordsFieldLabel
})
);
}
if (this.properties.useDefaultSearchQuery) {
defaultSearchQueryFields.push(
PropertyPaneTextField('defaultSearchQuery', {
label: strings.DefaultSearchQueryKeywordsFieldLabel,
description: strings.DefaultSearchQueryKeywordsFieldDescription,
multiline: true,
resizable: true,
placeholder: strings.SearchQueryPlaceHolderText,
onGetErrorMessage: this._validateEmptyField.bind(this),
deferredValidationTime: 500
})
);
}
return {
primaryGroup: {
groupFields: [
@ -561,20 +665,29 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
groupFields: [
PropertyPaneDynamicFieldSet({
label: strings.SearchQueryKeywordsFieldLabel,
fields: [
PropertyPaneDynamicField('queryKeywords', {
label: strings.SearchQueryKeywordsFieldLabel
label: strings.SearchQueryKeywordsFieldLabel
})
],
sharedConfiguration: {
depth: DynamicDataSharedDepth.Source,
}
})
]
},
}),
].concat(defaultSearchQueryFields)
},
// Show the secondary group only if the web part has been
// connected to a dynamic data source
showSecondaryGroup: !!this.properties.queryKeywords.tryGetSource(),
onShowPrimaryGroup: () => {
// Reset dynamic data fields related values to be consistent
this.properties.useDefaultSearchQuery = false;
this.properties.defaultSearchQuery = '';
this.properties.queryKeywords.setValue('');
this.render();
}
} as IPropertyPaneConditionalGroup;
}
@ -642,10 +755,6 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
dialogTitle: strings.DialogTitle,
saveButtonText: strings.SaveButtonText
}
}),
PropertyPaneToggle('useHandlebarsHelpers', {
label: "Handlebars Helpers",
checked: this.properties.useHandlebarsHelpers
})
];

View File

@ -48,9 +48,14 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
// Initialize the Office UI grouped list
this.props.availableFilters.map((filter, i) => {
// Get group name
let groupName = filter.FilterName;
const configuredFilter = this.props.refinersConfiguration.filter(e => { return e.refinerName === filter.FilterName;});
groupName = configuredFilter.length > 0 && configuredFilter[0].displayValue ? configuredFilter[0].displayValue : groupName;
groups.push({
key: i.toString(),
name: this.props.refinersConfiguration[filter.FilterName],
name: groupName,
count: 1,
startIndex: i,
isDropEnabled: true,
@ -133,12 +138,9 @@ export default class FilterPanel extends React.Component<IFilterPanelProps, IFil
isOpen={this.state.showPanel}
type={PanelType.custom}
customWidth="450px"
isBlocking={false}
isLightDismiss={true}
onDismiss={this._onClosePanel}
headerText={strings.FilterPanelTitle}
closeButtonAriaLabel='Close'
hasCloseButton={true}
onRenderBody={() => {
if (this.props.availableFilters.length > 0) {
return (

View File

@ -1,9 +1,10 @@
import { IRefinementResult } from '../../../../models/ISearchResult';
import RefinementFilterOperationCallback from '../../../../models/RefinementValueOperationCallback';
import IRefinerConfiguration from '../../../../models/IRefinerConfiguration';
interface IFilterPanelProps {
availableFilters: IRefinementResult[];
refinersConfiguration: { [key: string]: string };
refinersConfiguration: IRefinerConfiguration[];
onUpdateFilters: RefinementFilterOperationCallback;
resetSelectedFilters: boolean;
}

View File

@ -33,12 +33,16 @@ export default class SearchResultsTemplate extends React.Component<ISearchResult
}
public componentWillUnmount() {
this.resize.removeResizeListener(this.parentRef, this.onComponentResize);
try {
this.resize.removeResizeListener(this.parentRef, this.onComponentResize);
} catch (error) {}
}
public componentDidMount() {
this._updateTemplate(this.props);
this.resize.addResizeListener(this.parentRef, this.onComponentResize);
try {
this.resize.addResizeListener(this.parentRef, this.onComponentResize);
} catch (error) {}
}
public componentDidUpdate() {

View File

@ -3,6 +3,9 @@ import ITaxonomyService from '../../../../services/TaxonomyService/ITaxonomyServ
import { DisplayMode } from '@microsoft/sp-core-library';
import TemplateService from '../../../../services/TemplateService/TemplateService';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import IRefinerConfiguration from '../../../../models/IRefinerConfiguration';
import { Sort } from '@pnp/sp';
import ISortableFieldConfiguration from '../../../../models/ISortableFieldConfiguration';
interface ISearchResultsContainerProps {
@ -39,7 +42,7 @@ interface ISearchResultsContainerProps {
/**
* The sort order of the results
*/
sortList: string;
sortList: Sort[];
/**
* Enable SharePoint query rules
@ -54,12 +57,12 @@ interface ISearchResultsContainerProps {
/**
* The managed properties used as refiners for the query
*/
refiners: { [key: string]: string };
refiners: IRefinerConfiguration[];
/**
* The managed properties used as sortable fields for the query
*/
sortableFields: { [key: string]: string };
sortableFields: ISortableFieldConfiguration[];
/**
* Show the paging control

View File

@ -15,6 +15,9 @@ import SearchResultsTemplate from '../Layouts/SearchResultsTemplate';
import styles from '../SearchResultsWebPart.module.scss';
import { SortPanel } from '../SortPanel';
import { SortDirection } from "@pnp/sp";
import { ITermData, ITerm } from '@pnp/sp-taxonomy';
import LocalizationHelper from '../../../../helpers/LocalizationHelper';
import { Text } from '@microsoft/sp-core-library';
declare var System: any;
let FilterPanel = null;
@ -176,7 +179,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
this.props.searchService.selectedProperties = this.props.selectedProperties;
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
const refinerManagedProperties = this.props.refiners.map(e => { return e.refinerName ;}).join(',');
const searchResults = await this.props.searchService.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, this.state.currentPage);
const localizedFilters = await this._getLocalizedFilters(searchResults.RefinementResults);
@ -242,10 +245,10 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
this.props.searchService.selectedProperties = nextProps.selectedProperties;
const refinerManagedProperties = Object.keys(nextProps.refiners).join(',');
const refinerManagedProperties = nextProps.refiners.map(e => { return e.refinerName ;}).join(',');
// Reset sortlist
this.props.searchService.sortList = this.props.sortList;
this.props.searchService.sortList = nextProps.sortList;
// We reset the page number and refinement filters
const searchResults = await this.props.searchService.search(nextProps.queryKeywords, refinerManagedProperties, [], 1);
@ -282,7 +285,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
}
} else {
this.setState({
areResultsLoading: false
areResultsLoading: false,
lastQuery: '',
results: { RefinementResults: [], RelevantResults: [] },
});
}
} else {
@ -317,7 +322,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
areResultsLoading: true,
});
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
const refinerManagedProperties = this.props.refiners.map(e => { return e.refinerName ;}).join(',');
const searchResults = await
this.props.searchService.search(this.props.queryKeywords, refinerManagedProperties, newFilters, 1);
@ -348,9 +353,9 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
errorMessage:null
});
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
const refinerManagedProperties = this.props.refiners.map(e => { return e.refinerName ;}).join(',');
this.props.searchService.sortList = `${sortField}:${SortDirection[sortDirection].toLocaleLowerCase()}`;
this.props.searchService.sortList = [{Property: sortField, Direction: sortDirection}];
try
{
@ -386,7 +391,7 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
areResultsLoading: true,
});
const refinerManagedProperties = Object.keys(this.props.refiners).join(',');
const refinerManagedProperties = this.props.refiners.map(e => { return e.refinerName ;}).join(',');
const searchResults = await this.props.searchService.search(this.props.queryKeywords, refinerManagedProperties, this.state.selectedFilters, pageNumber);
@ -404,8 +409,12 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
*/
private async _getLocalizedFilters(rawFilters: IRefinementResult[]): Promise<IRefinementResult[]> {
// Get the current lcid according to current page language
const lcid = LocalizationHelper.getLocaleId(this.props.context.pageContext.cultureInfo.currentUICultureName);
let termsToLocalize: { uniqueIdentifier: string, termId: string, localizedTermLabel: string }[] = [];
let updatedFilters = [];
let localizedTerms = [];
rawFilters.map((filterResult) => {
@ -432,45 +441,49 @@ export default class SearchResultsContainer extends React.Component<ISearchConta
if (termsToLocalize.length > 0) {
// Process all terms in a single JSOM call for performance purpose. In general JSOM is pretty slow so we try to limit the number of calls...
await this.props.taxonomyService.initialize();
// Get the terms from taxonomy
// If a term doesn't exist anymore, it won't be retrieved by the API so the termValues count could be less than termsToLocalize count
const termValues = await this.props.taxonomyService.getTermsById(termsToLocalize.map((t) => { return t.termId; }));
const termsEnumerator = termValues.getEnumerator();
termsToLocalize.map((termToLocalize) => {
while (termsEnumerator.moveNext()) {
// Check if the term has been retrieved from taxonomy (i.e. exists)
const termsFromTaxonomy = termValues.filter((taxonomyTerm: ITerm & ITermData) => {
const termIdFromTaxonomy = taxonomyTerm.Id.substring(taxonomyTerm.Id.indexOf('(') + 1, taxonomyTerm.Id.indexOf(')'));
return termIdFromTaxonomy === termToLocalize.termId;
});
const currentTerm = termsEnumerator.get_current();
if (termsFromTaxonomy.length > 0) {
// Need to do this check in the case where the term indexed by the search doesn't exist anymore in the term store
if (!currentTerm.get_serverObjectIsNull()) {
// Should be always unique since we can't have two terms with the same ids
const termFromTaxonomy: ITerm & ITermData = termsFromTaxonomy[0];
const termId = currentTerm.get_id();
// Check if retrieved term is part of terms to localize
const terms = termsToLocalize.filter((e) => { return e.termId === termId.toString(); });
if (terms.length > 0) {
termsToLocalize = termsToLocalize.map((term) => {
if (term.termId === terms[0].termId) {
return {
uniqueIdentifier: term.uniqueIdentifier,
termId: termId.toString(),
localizedTermLabel: termsEnumerator.get_current().get_name(),
};
} else {
return term;
}
});
}
// It supposes the 'Label' property has been selected in the underlying call
// A term always have a default label so the collection can't be empty
const localizedLabel = termFromTaxonomy["Labels"]._Child_Items_.filter((label: any) => {
return label.Language === lcid;
});
localizedTerms.push({
uniqueIdentifier: termToLocalize.uniqueIdentifier,
termId: termToLocalize.termId,
localizedTermLabel: localizedLabel.length > 0 ? localizedLabel[0].Value : termFromTaxonomy.Name
});
} else {
localizedTerms.push({
uniqueIdentifier: termToLocalize.uniqueIdentifier,
termId: termToLocalize.termId,
localizedTermLabel: Text.format(strings.TermNotFound, termToLocalize.termId)
});
}
}
});
// Update original filters with localized values
rawFilters.map((filter) => {
let updatedValues = [];
filter.Values.map((value) => {
const existingFilters = termsToLocalize.filter((e) => { return e.uniqueIdentifier === value.RefinementToken; });
const existingFilters = localizedTerms.filter((e) => { return e.uniqueIdentifier === value.RefinementToken; });
if (existingFilters.length > 0) {
updatedValues.push({
RefinementCount: value.RefinementCount,

View File

@ -30,6 +30,11 @@
color: "[theme: themePrimary, default: #005a9e]";
}
&__sortDropdown {
min-width: 135px;
max-width: 200px;
}
&__sortResultBtn {
color: "[theme: themePrimary]";
}

View File

@ -1,8 +1,9 @@
import UpdateSortOperationCallback from '../../../../models/UpdateSortOperationCallback';
import { SortDirection } from "@pnp/sp";
import ISortableFieldConfiguration from '../../../../models/ISortableFieldConfiguration';
interface ISortPanelProps {
sortableFieldsConfiguration: { [key: string]: string };
sortableFieldsConfiguration: ISortableFieldConfiguration[];
onUpdateSort: UpdateSortOperationCallback;
sortDirection?:SortDirection;
sortField?:string;

View File

@ -4,7 +4,7 @@ import ISortPanelState from './ISortP
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import * as strings from 'SearchResultsWebPartStrings';
import { ActionButton } from 'office-ui-fabric-react/lib/Button';
import { SortDirection } from "@pnp/sp";
import { SortDirection } from '@pnp/sp';
import styles from '../SearchResultsWebPart.module.scss';
export default class SortPanel extends React.Component<ISortPanelProps, ISortPanelState> {
@ -17,14 +17,13 @@ export default class SortPanel extends React.Component<ISortPanelProps, ISortPan
sortField:this.props.sortField ? this.props.sortField : null
};
this._getSortableFieldCount = this._getSortableFieldCount.bind(this);
this._setSortDirection = this._setSortDirection.bind(this);
this._getDropdownOptions = this._getDropdownOptions.bind(this);
this._onChangedSelectedField = this._onChangedSelectedField.bind(this);
}
public render(): React.ReactElement<ISortPanelProps> {
if (this._getSortableFieldCount() === 0) return <span />;
if (this.props.sortableFieldsConfiguration.length === 0) return <span />;
const dropdownOptions: IDropdownOption[] = this._getDropdownOptions();
@ -42,6 +41,7 @@ export default class SortPanel extends React.Component<ISortPanelProps, ISortPan
}}
/>
<Dropdown
className={ styles.searchWp__sortDropdown }
placeHolder={strings.Sort.SortPanelSortFieldPlaceHolder}
ariaLabel={strings.Sort.SortPanelSortFieldAria}
onChanged={this._onChangedSelectedField}
@ -52,14 +52,6 @@ export default class SortPanel extends React.Component<ISortPanelProps, ISortPan
);
}
private _getSortableFieldCount() {
if(!this.props.sortableFieldsConfiguration) return 0;
return Object.keys(this.props.sortableFieldsConfiguration).filter(value => {
return value;
}).length;
}
private _setSortDirection() {
let sortDirection;
@ -86,14 +78,16 @@ export default class SortPanel extends React.Component<ISortPanelProps, ISortPan
}
private _getDropdownOptions():IDropdownOption[] {
let dropdownOptions:IDropdownOption[] = [];
const sortableFields = Object.keys(this.props.sortableFieldsConfiguration);
sortableFields.forEach((fieldKey) => {
//Strip " from start and end of the display name if present
const fieldDisplayName = this.props.sortableFieldsConfiguration[fieldKey].replace(/^\"+|\"+$/g, '');
dropdownOptions.push({ key: fieldKey, text: fieldDisplayName});
let dropdownOptions:IDropdownOption[] = [];
this.props.sortableFieldsConfiguration.map(e => {
dropdownOptions.push({
key: e.sortField,
text: e.displayValue
});
});
return dropdownOptions;
}

View File

@ -8,12 +8,8 @@ define([], function() {
"LoadingMessage": "Results are loading, please wait...",
"MaxResultsCount": "Number of items to retrieve per page",
"NoResultMessage": "There are no results to show",
"RefinersFieldLabel": "Refiners",
"SortableFieldsLabel": "Sortable fields",
"FilterPanelTitle": "Available filters",
"SortPanelTitle":"Sort",
"FilterResultsButtonLabel": "Filters",
"SortResultsButtonLabel":"Sort",
"SelectedFiltersLabel": "Selected filters:",
"RemoveAllFiltersLabel": "Remove all filters",
"ShowPagingLabel": "Show paging",
@ -31,8 +27,6 @@ define([], function() {
"InvalidResultSourceIdMessage": "Invalid identifier",
"EnableQueryRulesLabel": "Enable query rules",
"StylingSettingsGroupName": "Styling options",
"RefinersFieldDescription": "Specifies managed properties used as refiners (ordered comma-separated list). You can specify the label by using the following format <Managed Property Name>:\"My friendly name\".",
"SortableFieldsDescription": "Specifies sortable properties used by the sort panel (ordered comma-separated list). You can specify the label by using the following format <Managed Property Name>:\"My friendly name\".",
"SelectedPropertiesFieldDescription": "Speficies the properties to retrieve from the search results.",
"SearchQueryKeywordsFieldDescription": "Use pre-defined search query keywords to retrieve a static set of results.",
"CountMessageLong": "<b>{0}</b> results for '<em>{1}</em>'",
@ -54,8 +48,10 @@ define([], function() {
"PromotedResultsLabel": "Promoted result(s)",
"PanelCloseButtonAria":"Close",
"Sort": {
"SortList": "Initial sort order",
"SortListDescription": "Specify initial sort order in a comma separated list on the format <Managed Property Name>:ascending/descending (default:Created:descending,Size:ascending).",
"SortableFieldsPropertyPaneField": "Sortable properties",
"SortableFieldsDescription": "Specifies sortable properties that users can use in the UI. Only one property can be used at a time for sorting and will override the search order specified in the WP if exists.",
"SortPropertyPaneFieldLabel": "Sort order",
"SortListDescription": "Specify the sort order for the search results. This will only applied when no manual filters have been set (i.e. sortable fields)",
"SortDirectionAscendingLabel":"Ascending",
"SortDirectionDescendingLabel":"Descending",
"SortErrorMessage":"Invalid search property (Check if the managed property is sortable).",
@ -63,6 +59,19 @@ define([], function() {
"SortPanelSortFieldAria":"Select a field",
"SortPanelSortFieldPlaceHolder":"Select a field",
"SortPanelSortDirectionLabel":"Sort Direction",
}
"SortableFieldManagedPropertyField": "Sort managed property",
"SortableFieldDisplayValueField": "Field name to display"
},
"Refiners": {
"RefinersFieldLabel": "Refiners",
"RefinerManagedPropertyField": "Filter managed property",
"RefinerDisplayValueField": "Filter name to display",
"RefinersFieldDescription": "Specifies managed properties used as refiners. If there are no values for a filter property, it won't appear in the panel.",
},
"TermNotFound": "(Term with ID '{0}' not found)",
"UseDefaultSearchQueryKeywordsFieldLabel": "Use a default search query",
"DefaultSearchQueryKeywordsFieldLabel": "Default search query",
"DefaultSearchQueryKeywordsFieldDescription": "This query will be used when the data source value is still empty.",
"ConfigureBtnLabel": "Configure"
}
});

View File

@ -8,12 +8,8 @@ define([], function() {
"LoadingMessage": "Les résultats sont en cours de chargement, veuillez patienter...",
"MaxResultsCount": "Nombre de résulats à récupérer par page",
"NoResultMessage": "Il n'y a aucun résultat à afficher.",
"RefinersFieldLabel": "Filtres",
"SortableFieldsLabel": "Triables",
"FilterPanelTitle": "Filtres disponibles",
"SortPanelTitle":"Trier",
"FilterResultsButtonLabel": "Filtrer",
"SortResultsButtonLabel":"Trier",
"SelectedFiltersLabel": "Filtre(s) appliqué(s):",
"RemoveAllFiltersLabel": "Supprimer tous les filtres",
"ShowPagingLabel": "Afficher la pagination",
@ -31,8 +27,6 @@ define([], function() {
"InvalidResultSourceIdMessage": "Identifiant invalide",
"EnableQueryRulesLabel": "Activer les règles de requête",
"StylingSettingsGroupName": "Options d'affichage",
"RefinersFieldDescription": "Propriétés gerées à utiliser comme filtres (liste ordonnée séparée par une virgule). Vous pouvez spécifier un label personnalisé en utilisant le format suivant <Nom de la propriété gérée>:\"Nom convivial\".",
"SortableFieldsDescription": "Propriétés gerées à utiliser comme triables (liste ordonnée séparée par une virgule). Vous pouvez spécifier un label personnalisé en utilisant le format suivant <Nom de la propriété gérée>:\"Nom convivial\".",
"SelectedPropertiesFieldDescription": "Propriétés à récupérer des résulats de recherche.",
"SearchQueryKeywordsFieldDescription": "Utilisez une requête de recherche prédéfinie pour obtenir un ensemble de résultats statique.",
"CountMessageLong": "<b>{0}</b> résultats pour '<em>{1}</em>'",
@ -54,15 +48,30 @@ define([], function() {
"PromotedResultsLabel": "Résultat(s) promu(s)",
"PanelCloseButtonAria":"Proche",
"Sort": {
"SortList": "Ordre de tri",
"SortListDescription": "Spécifiez l'ordre de tri dans une liste séparée par des virgules au format <Nom de la propriété gérée>:ascending/descending (par défaut:Created:descending,Size:ascending).",
"SortableFieldsPropertyPaneField":"Propriétés triables",
"SortableFieldsDescription": "Propriétés à utiliser pour permettre aux utilisateurs de trier les résultats depuis l'interface. Le tri ne peut être porter que sur une seule propriété à la fois et surpasse le tri par défaut des résulats si existant.",
"SortPropertyPaneFieldLabel":"Ordre de tri",
"SortListDescription": "Spécifiez l'ordre de tri des résultats de recherche. Ceux-ci ne s'appliqueront que si aucun champ de tri n'a été configurée pour ce Web Part (i.e propriétés triables)",
"SortDirectionAscendingLabel":"Ascendant",
"SortDirectionDescendingLabel":"Descendant",
"SortErrorMessage":"Propriété de recherche non valide (Vérifiez si la propriété managée est triable).",
"SortPanelSortFieldLabel":"Trier sur le champ",
"SortPanelSortFieldAria":"Sélectionner un champ",
"SortPanelSortFieldPlaceHolder":"Sélectionner un champ",
"SortPanelSortDirectionLabel":"Direction de tri",
}
"SortPanelSortDirectionLabel":"Direction de tri",
"SortableFieldManagedPropertyField": "Propriété gérée de tri",
"SortableFieldDisplayValueField": "Intitulé du champ à afficher"
},
"Refiners": {
"RefinersFieldLabel": "Filtres",
"RefinersFieldDescription": "Configurez ici les propriétés gerées à utiliser comme filtres. Si il n'existe pas de valeurs pour le filtre spécifié, il n'apparaîtra pas dans le panneau.",
"RefinerManagedPropertyField": "Propriété gérée de filtre",
"RefinerDisplayValueField": "Intitulé du filtre à afficher",
},
"TermNotFound": "(Terme avec l'ID '{0}' non trouvé)",
"UseDefaultSearchQueryKeywordsFieldLabel": "Utiliser une requête initiale",
"DefaultSearchQueryKeywordsFieldLabel": "Requête de recherche par défaut",
"DefaultSearchQueryKeywordsFieldDescription": "Cette requête sera utilisée par défault dans le cas où la valeur de la source de données est encore vide.",
"ConfigureBtnLabel": "Configurer"
}
});

View File

@ -9,14 +9,10 @@ declare interface ISearchResultsWebPartStrings {
LoadingMessage: string;
MaxResultsCount: string;
NoResultMessage: string;
RefinersFieldLabel: string;
SortableFieldsLabel: string;
RefinersFieldDescription: string;
SortableFieldsDescription: string;
FilterPanelTitle: string;
SortPanelTitle: string;
FilterResultsButtonLabel: string;
SortResultsButtonLabel:string;
SelectedFiltersLabel: string;
RemoveAllFiltersLabel: string;
ShowPagingLabel: string;
@ -52,8 +48,9 @@ declare interface ISearchResultsWebPartStrings {
HandlebarsHelpersDescription: string;
PromotedResultsLabel: string;
PanelCloseButtonAria:string;
ConfigureBtnLabel: string;
Sort: {
SortList: string;
SortPropertyPaneFieldLabel
SortListDescription: string;
SortDirectionAscendingLabel:string;
SortDirectionDescendingLabel:string;
@ -61,8 +58,22 @@ declare interface ISearchResultsWebPartStrings {
SortPanelSortFieldLabel:string;
SortPanelSortFieldAria:string;
SortPanelSortFieldPlaceHolder:string;
SortPanelSortDirectionLabel:string;
}
SortPanelSortDirectionLabel:string;
SortableFieldsPropertyPaneField: string;
SortableFieldsDescription: string;
SortableFieldManagedPropertyField: string;
SortableFieldDisplayValueField: string;
},
Refiners: {
RefinersFieldLabel: string;
RefinersFieldDescription: string;
RefinerManagedPropertyField: string;
RefinerDisplayValueField: string;
},
TermNotFound: string;
UseDefaultSearchQueryKeywordsFieldLabel: string;
DefaultSearchQueryKeywordsFieldLabel: string;
DefaultSearchQueryKeywordsFieldDescription: string;
}
declare module 'SearchResultsWebPartStrings' {