* c

* we

* working

* work on searchable props

* organize

* workinng

* accept siteUrl as parameter

* qw

* a

* work on display

* work on propertyBagDisplay

* ZX

* qwe

* qwd

* qdw

* qdw

* refactoring

* qw

* qw

* qwd

* awd

* er

* git popup on disoplay working

* 123

* qw

* asd

* works

* Working!

* qweqwwe

* asd

* linting

* tslint

* enable edit all properties

* edit all props

* sort not quite working

* sort working

* sort working

* now really!

* fdg

* Added MessageDisplay

* separete messagedisplay for panel

* added PropertyBagEditPanel

* Implement fileters site list

* new props

* Filters working

* add addn'l parameters

* extracted User Filetrs

* added dropdowns to coimmand bar

* qw

* filters

* ads

* qwe

* sdf

* work on user filters

* dwq

* qw

* add global nab menu

* filterstate

* qwe

* qwe

* fixed filters on filtered site list

* convert params in webparr

* work on men us

* fix nav webpart. Cleanup Descriptions

* clean linting errors

* qwe

* added comments

* fix href in menu

* readme

* fix render of bolean in list

* add images

* Update README.md

* Update README.md

* Update README.md

* fix copy-assets.json

* cleaned up webpart names and descriptions

* fix issue with menu and blank props

* fix readme
This commit is contained in:
Russell gove 2017-03-28 02:08:45 -04:00 committed by Vesa Juvonen
parent 17753ee571
commit 7c272c561c
79 changed files with 20244 additions and 0 deletions

29
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceRoot}/app.js"
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${file}",
"outFiles": []
},
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"address": "localhost",
"port": 5858,
"outFiles": []
}
]
}

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 @@
* text=auto

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,14 @@
# Folders
.vscode
coverage
node_modules
sharepoint
src
temp
# Files
*.csproj
.git*
.yo-rc.json
gulpfile.js
tsconfig.json

View File

@ -0,0 +1,7 @@
{
"@microsoft/generator-sharepoint": {
"libraryName": "react-property-bag-editor",
"libraryId": "12dac38e-b255-44ce-9f06-050571b34d39",
"framework": "react"
}
}

View File

@ -0,0 +1,153 @@
# Property Bag Navigation Webparts
## Summary
A set of webparts that lets you set property bag settings on site collections and enable navigation using those properties.
[picture of the web part in action]
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https://blogs.office.com/2017/02/23/sharepoint-framework-reaches-general-availability-build-and-deploy-engaging-web-parts-today/)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
> pnp-js-core
## Solution
Solution|Author(s)
--------|---------
react-property-bag-editor| Russell Gove
## Version history
Version|Date|Comments
-------|----|--------
1.0|march 19, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
> Include any additional steps as needed.
## Features
This project consists of four webparts that can be used to manage the Property Bags for SharePoint sites and display navigational components from those Properties.
- PropertyBagEditor
This webpart allows an administrator to edit selected items in a site's Property Bag. A sample display is shown below:
![PropertyBagEditorDisplay](./src/images/PropertyBagEditorDisplay.PNG)
Selecting a Property and clicking the Edit button will bring up the Edit Panel:
![PropertyBagEditorEdur](./src/images/PropertyBagEditorEdit.PNG)
Here you can change the value of the property and specify if the property should be included in the search Index.
The Properties that can be edited are specified in the webpart's Property Pane:
![PropertyBagEditorEdur](./src/images/PropertyBagEditorConfig.PNG)
The Properties set in the Property Pane of this webpart are crawled properties, and should be mapped to managed properties so that can be used by the other webparts in this project.
The Site whose properties are to be edited can be passed in via a query parameter. While this webpart can be added to any page, it would be most useful if added to a page in an infrastructure site collection in the tenant, and then linked to from all other sites via a link in the Site Settings page.
The following script shows how to add such a link to all sites 'Site Settings' page using PNP Powershell. It will add a menu item named 'Edit Site Metadata ' to the Site Settings of each Team Site that links to the PropertBagEdcitor.aspx page on the tenants infrastructure site (this site is named 'cdn' in the example below).
$adminSiteUrl="https://tenant-admin.sharepoint.com"
$customActionDescription="CUSTOM_\ ___Navigation__ \__Metadata"
$pageUrl="https://tenant.sharepoint.com/sites/cdn/SitePages/PropertBagEdcitor.aspx?siteUrl={0}"
$credentials=get-credential
Connect-SPOnline -Url $adminSiteUrl -Credentials $credentials
$ctx = Get-SPOContext
$sites = Get-SPOTenantSite -Detailed
foreach($site in $sites){
if ($site.Template -eq "STS#0") {
Connect-SPOnline Url $site.Url Credentials $credentials
$existing = Get-SPOCustomAction -Scope "Site" | ? { $\_.Description -eq $customActionDescription }
if ($existing) {
$existing.DeleteObject();
Execute-SPOQuery;
Write-Host "Deleteting existing action from"$site.Url
}
Write-Host "adding action to" $site.Url
Add-SPOCustomAction -Description $customActionDescription -Location "Microsoft.SharePoint.SiteSettings" -name "Edit Site Metadata"-Title "Edit Site Metadata" -Group "SiteAdministration" -Sequence 10100 -Url ( [string]::Format($pageUrl,$site.Url)) -Scope "Site"
Execute-SPOQuery
}
}
- PropertyBagDisplay
The propertyBagDisplay webpart can be used by an administrator to view and edit selected properties across sites in the tenant:
![PropertyBagDisplay](./src/images/PropertyBagDisplayDisplay.PNG)
In the Property Pane, an administrator must specify both the Crawled Property Name and the Managed Property name (separated by a pipe character) of the properties to be included in the webpart:
![PropertyBagDisplayConfig](./src/images/PropertBagDisplayConfig.PNG)
The administrator can also include a list of site templates to narrow down the list of sites to be included in the webpart. When specifying site templates to include you can include just the Site Template Name (STS) and all sites within that template name will be included, or you can specify the Site Template Name and ID, separated by a '#" character (STS#1) to have only sites with that template name and ID included.
The webpart displays the site template, Title and Url, plus the selected Managed Properties for all sites in the tenant with the selected site template. The Managed Properties are only displayed if they have been set as searchable, and a full crawl has been run. After selecting a Site, a user can click the edit button to edit the Crawled properties (i.e. the raw property bag values) for the selected site:
![PropertyBagDisplayEdit](./src/images/PropertyBagDisplayEdit.PNG)
On the edit panel one can specify a new value for each property as well as whether that property is to be included in the search index. Additionally one can specify that a full crawl of the site should be run once the properties are saved.
- PropertyBagFilteredSiteList
This webpart displays a list of all sites that meet the criteria specified in the property pane by the administrator:
![PropertyBagFilteredSiteListDisplay](./src/images/PropertyBagFilteredSiteListDisplay.PNG)
Additionally, it lets the user narrow down the list of sites displayed by applying metadata filters that are set up by the administrator in the Property Pane( Businsess Unit and Continent in the example above):
![PropertyBagFilteredSiteListConfig](./src/images/PropertyBagFilteredSiteListConfigy.PNG)
In the PropertyPane above, the 'Site Templates to Include' and 'Metadata Filters' are used to filter which site collections are retrieved from search. The 'User Filters' are used to allow the user to easily filter the results returned from search using the command bar on the top of the display.
- PropertyBagGlobalNav
This Webpart builds a navigation menu based on the Managed Properties set up in the PropertyPane:
![propertyBagGlobalNavDisplay](./src/images/propertyBagGlobalNavDisplay.PNG)
In the PropertyPane, an administrator just needs to specify which Managed Properties are to be used to build the Navigation menu:
![PropertyBagGlobalNavConfig](./src/images/PropertyBagGlobalNavConfig.PNG)
If desired, the admin can also specify which site templates should be included in the menu, as wall as any additional filters. Additional Filteres can be specified in the format 'ManagedPropertyName=value';
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />

View File

@ -0,0 +1,133 @@
## react-property-bag-editor
This project consists of four webparts that can be used to manage the Property Bags for SharePoint sites and display navigational components from those Properties.
PropertyBagEditor.
This webpart allows an administrator to edit selected items in a site&#39;s Property Bag. A sample display is shown below:
![PropertyBagEditorDisplay](./src/images/PropertyBagEditorDisplay.PNG)
Selecting a Property and clicking the Edit button will bring up the Edit Panel:
![PropertyBagEditorEdur](./src/images/PropertyBagEditorEdit.PNG)
Here you can change the value of the property and specify if the property should be included in the search Index.
The Properties that can be edited are specified in the webpart&#39;s Property Pane:
![PropertyBagEditorEdur](./src/images/PropertyBagEditorConfig.PNG)
The Properties set in the Property Pane of this webpart are crawled properties, and should be mapped to managed properties so that can be used by the other webparts in this project.
The Site whose properties are to be edited can be passed in via a query parameter. While this webpart can be added to any page, it would be most useful if added to a page in an infrastructure site collection in the tenant, and then linked to from all other sites via a link in the Site Settings page.
The following script shows how to add such a link to all sites &#39;Site Settings&#39; page using PNP Powershell. It will add a menu item named &#39;Edit Site Metadata &#39; to the Site Settings of each Team Site that links to the PropertBagEdcitor.aspx page on the tenants infrastructure site (this site is named 'cdn' in the example below).
$adminSiteUrl=&quot;https://tenant-admin.sharepoint.com&quot;
$customActionDescription=&quot;CUSTOM_\ ___Navigation__ \__Metadata&quot;
$pageUrl=&quot;https://tenant.sharepoint.com/sites/cdn/SitePages/PropertBagEdcitor.aspx?siteUrl={0}&quot;
$credentials=get-credential
Connect-SPOnline -Url $adminSiteUrl -Credentials $credentials
$ctx = Get-SPOContext
$sites = Get-SPOTenantSite -Detailed
foreach($site in $sites){
if ($site.Template -eq &quot;STS#0&quot;) {
Connect-SPOnline Url $site.Url Credentials $credentials
$existing = Get-SPOCustomAction -Scope &quot;Site&quot; | ? { $\_.Description -eq $customActionDescription }
if ($existing) {
$existing.DeleteObject();
Execute-SPOQuery;
Write-Host &quot;Deleteting existing action from&quot;$site.Url
}
Write-Host &quot;adding action to&quot; $site.Url
Add-SPOCustomAction -Description $customActionDescription -Location &quot;Microsoft.SharePoint.SiteSettings&quot; -name &quot;Edit Site Metadata&quot;-Title &quot;Edit Site Metadata&quot; -Group &quot;SiteAdministration&quot; -Sequence 10100 -Url ( [string]::Format($pageUrl,$site.Url)) -Scope &quot;Site&quot;
Execute-SPOQuery
}
}
PropertyBagDisplay
The propertyBagDisplay webpart can be used by an administrator to view and edit selected properties across sites in the tenant:
![PropertyBagDisplay](./src/images/PropertyBagDisplayDisplay.PNG)
In the Property Pane, an administrator must specify both the Crawled Property Name and the Managed Property name (separated by a pipe character) of the properties to be included in the webpart:
![PropertyBagDisplayConfig](./src/images/PropertBagDisplayConfig.PNG)
The administrator can also include a list of site templates to narrow down the list of sites to be included in the webpart. When specifying site templates to include you can include just the Site Template Name (STS) and all sites within that template name will be included, or you can specify the Site Template Name and ID, separated by a &#39;#&quot; character (STS#1) to have only sites with that template name and ID included.
The webpart displays the site template, Title and Url, plus the selected Managed Properties for all sites in the tenant with the selected site template. The Managed Properties are only displayed if they have been set as searchable, and a full crawl has been run. After selecting a Site, a user can click the edit button to edit the Crawled properties (i.e. the raw property bag values) for the selected site:
![PropertyBagDisplayEdit](./src/images/PropertyBagDisplayEdit.PNG)
On the edit panel one can specify a new value for each property as well as whether that property is to be included in the search index. Additionally one can specify that a full crawl of the site should be run once the properties are saved.
PropertyBagFilteredSiteList
This webpart displays a list of all sites that meet the criteria specified in the property pane by the administrator:
![PropertyBagFilteredSiteListDisplay](./src/images/PropertyBagFilteredSiteListDisplay.PNG)
Additionally, it lets the user narrow down the list of sites displayed by applying metadata filters that are set up by the administrator in the Property Pane( Businsess Unit and Continent in the example above):
![PropertyBagFilteredSiteListConfig](./src/images/PropertyBagFilteredSiteListConfigy.PNG)
In the PropertyPane above, the 'Site Templates to Include' and 'Metadata Filters' are used to filter which site collections are retrieved from search. The 'User Filters' are used to allow the user to easily filter the results returned from search using the command bar on the top of the display.
PropertyBagGlobalNav
This Webpart builds a navigation menu based on the Managed Properties set up in the PropertyPane:
![propertyBagGlobalNavDisplay](./src/images/propertyBagGlobalNavDisplay.PNG)
In the PropertyPane, an administrator just needs to specify which Managed Properties are to be used to build the Navigation menu:
![PropertyBagGlobalNavConfig](./src/images/PropertyBagGlobalNavConfig.PNG)
If desired, the admin can also specify which site templates should be included in the menu, as wall as any additional filters. Additional Filteres can be specified in the format 'ManagedPropertyName=value';
### Building the code
```bash
git clone the repo
npm i
npm i -g gulp
gulp
```
This package produces the following:
* lib/* commonjs components - this allows this package to be reused from other packages.
* dist/* - a single bundle containing the components used for uploading to a cdn pointing a registered Sharepoint webpart library to.
* example/* a test page that hosts all components in this package.
### Build options
gulp clean - TODO
gulp test - TODO
gulp watch - TODO
gulp build - TODO
gulp deploy - TODO

View File

@ -0,0 +1,58 @@
{
"entries": [
{
"entry": "./lib/webparts/propertyBagEditor/PropertyBagEditorWebPart.js",
"manifest": "./src/webparts/propertyBagEditor/PropertyBagEditorWebPart.manifest.json",
"outputPath": "./dist/property-bag-editor.bundle.js"
},
{
"entry": "./lib/webparts/propertyBagDisplay/PropertyBagDisplayWebPart.js",
"manifest": "./src/webparts/propertyBagDisplay/PropertyBagDisplayWebPart.manifest.json",
"outputPath": "./dist/property-bag-display.bundle.js"
},
{
"entry": "./lib/webparts/propertyBagFilteredSiteList/PropertyBagFilteredSiteListWebPart.js",
"manifest": "./src/webparts/propertyBagFilteredSiteList/PropertyBagFilteredSiteListWebPart.manifest.json",
"outputPath": "./dist/property-bag-filtered-site-list.bundle.js"
},
{
"entry": "./lib/webparts/propertyBagGlobalNav/PropertyBagGlobalNavWebPart.js",
"manifest": "./src/webparts/propertyBagGlobalNav/PropertyBagGlobalNavWebPart.manifest.json",
"outputPath": "./dist/property-bag-global-nav.bundle.js"
}
],
"externals": {
"sp-pnp-js": "https://cdnjs.cloudflare.com/ajax/libs/sp-pnp-js/2.0.1/pnp.min.js",
"sp-init": {
"path": "https://rgove3.sharepoint.com/_layouts/15/init.js",
"globalName": "$_global_init"
},
"microsoft-ajax": {
"path": "https://rgove3.sharepoint.com/_layouts/15/MicrosoftAjax.js",
"globalName": "Sys",
"globalDependencies": [
"sp-init"
]
},
"sp-runtime": {
"path": "https://rgove3.sharepoint.com/_layouts/15/SP.Runtime.js",
"globalName": "SP",
"globalDependencies": [
"microsoft-ajax"
]
},
"sharepoint": {
"path": "https://rgove3.sharepoint.com/_layouts/15/SP.js",
"globalName": "SP",
"globalDependencies": [
"sp-runtime"
]
}
},
"localizedResources": {
"propertyBagEditorStrings": "webparts/propertyBagEditor/loc/{locale}.js",
"propertyBagDisplayStrings": "webparts/propertyBagDisplay/loc/{locale}.js",
"propertyBagFilteredSiteListStrings": "webparts/propertyBagFilteredSiteList/loc/{locale}.js",
"propertyBagGlobalNavStrings": "webparts/propertyBagGlobalNav/loc/{locale}.js"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,6 @@
{
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-property-bag-editor",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,10 @@
{
"solution": {
"name": "react-property-bag-editor-client-side-solution",
"id": "12dac38e-b255-44ce-9f06-050571b34d39",
"version": "1.0.0.0"
},
"paths": {
"zippedPackage": "solution/react-property-bag-editor.sppkg"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,9 @@
{
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

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

View File

@ -0,0 +1,3 @@
{
"cdnBasePath": "https://rgove3.sharepoint.com/sites/cdn/spfxapps/propertybageditor"
}

View File

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

View File

@ -0,0 +1,39 @@
{
"name": "react-property-bag-editor",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-client-base": "~1.0.0",
"@microsoft/sp-core-library": "~1.0.0",
"@microsoft/sp-webpart-base": "~1.0.0",
"@types/react": "0.14.46",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"lodash": "^4.17.4",
"office-ui-fabric-react": "^0.69.0",
"react": "0.14.8",
"react-dom": "0.14.8",
"sp-pnp-js": "^2.0.1",
"typescript": "^2.1.5"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.0.0",
"@microsoft/sp-module-interfaces": "~1.0.0",
"@microsoft/sp-webpart-workbench": "~1.0.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,5 @@
var context = require.context('.', true, /.+\.test\.js?$/);
context.keys().forEach(context);
module.exports = context;

View File

@ -0,0 +1,5 @@
export interface IPropertyBagDisplayWebPartProps {
description: string;
propertiesToDisplay: string; //separated by \n
siteTemplatesToInclude: string; //STS#1 STS#2 separated by \n leave off the #1 to get all STS
}

View File

@ -0,0 +1,22 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "fa63037d-d7bd-4d52-894a-b40127773283",
"alias": "PropertyBagDisplayWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [{
"groupId": "fa63037d-d7bd-4d52-894a-b40127773283",
"group": { "default": "Property Bag Navigation" },
"title": { "default": "Property Bag Display" },
"description": { "default": "Displays all Sites and selected properties, an lets you edit those propertiies" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "propertyBagDisplay",
"propertiesToDisplay":
"CUSTOMNAVAreaName|AreaName\nCUSTOMNAVBusinessUnit|BusinessUnit\nCUSTOMNAVContinent|Continent\nCUSTOMNAVLocation|Location",
"siteTemplatesToInclude":
"STS\nPUBLISHING"
}
}]
}

View File

@ -0,0 +1,85 @@
import * as React from "react";
import pnp from "sp-pnp-js";
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 "propertyBagDisplayStrings";
import PropertyBagDisplay from "./components/PropertyBagDisplay";
import { IPropertyBagDisplayProps } from "./components/IPropertyBagDisplayProps";
import { IPropertyBagDisplayWebPartProps } from "./IPropertyBagDisplayWebPartProps";
import utils from "../shared/utils";
export default class PropertyBagDisplayWebPart extends BaseClientSideWebPart<IPropertyBagDisplayWebPartProps> {
/**
* Renders the component.
*
* converts the new-line (\n) separated strings to an array of
* strings to be passed to the component.
*
*
* @memberOf PropertyBagDisplayWebPart
*/
public render(): void {
const element: React.ReactElement<IPropertyBagDisplayProps> = React.createElement(
PropertyBagDisplay,
{
description: this.properties.description,
propertiesToDisplay: utils.parseMultilineTextToArray(this.properties.propertiesToDisplay),
siteTemplatesToInclude:utils.parseMultilineTextToArray(this.properties.siteTemplatesToInclude)
}
);
ReactDom.render(element, this.domElement);
}
public onInit(): Promise<void> {
return super.onInit().then(_ => {
pnp.setup({
spfxContext: this.context
});
});
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("description", {
label: strings.DescriptionFieldLabel
}),
PropertyPaneTextField("propertiesToDisplay", {
label: strings.PropertiesToDisplayFieldLabel,
multiline: true
}),
PropertyPaneTextField("siteTemplatesToInclude", {
label: strings.SiteTemplatesToIncludeFieldLabel,
multiline: true
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,5 @@
export interface IPropertyBagDisplayProps {
description: string;
propertiesToDisplay: Array<string>;
siteTemplatesToInclude:Array< string>; //STS#1 STS#2 separated by \n leave off the #1 to get all STS
}

View File

@ -0,0 +1,23 @@
.helloWorld {
.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 {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
text-decoration: none;
}
}

View File

@ -0,0 +1,572 @@
import * as React from "react";
import pnp from "sp-pnp-js";
import { Web } from "sp-pnp-js";
import * as _ from "lodash";
import utils from "../../shared/utils";
import DisplayProp from "../../shared/DisplayProp";
import { SearchQuery, SearchResults } from "sp-pnp-js";
import { css } from "office-ui-fabric-react";
//import styles from "./PropertyBagDisplay.module.scss";
import { IPropertyBagDisplayProps } from "./IPropertyBagDisplayProps";
import { CommandBar } from "office-ui-fabric-react/lib/CommandBar";
import { Label } from "office-ui-fabric-react/lib/Label";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Toggle } from "office-ui-fabric-react/lib/Toggle";
import { Button, ButtonType } from "office-ui-fabric-react/lib/Button";
import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar";
import * as md from "../../shared/MessageDisplay";
import MessageDisplay from "../../shared/MessageDisplay";
import {
DetailsList, DetailsListLayoutMode, IColumn, IGroupedList, SelectionMode, CheckboxVisibility, IGroup
} from "office-ui-fabric-react/lib/DetailsList";
import {
GroupedList
} from "office-ui-fabric-react/lib/GroupedList";
import {
IViewport
} from "office-ui-fabric-react/lib/utilities/decorators/withViewport";
import {
Panel, PanelType
} from "office-ui-fabric-react/lib/Panel";
import { IContextualMenuItem, } from "office-ui-fabric-react/lib/ContextualMenu";
export interface IPropertyBagDisplayState {
selectedIndex: number; // the currently selected site
managedToCrawedMapping?: Array<ManagedToCrawledMappingEntry>;// Determines which Carwled propeties are mapped to which Managed Properties
errorMessages: Array<md.Message>; // a list of error massages displayed on the page
isediting?: boolean; //Determines if the edit panel is displayed
sites: Array<any>; // the list of sites displayed in the component
workingStorage?: DisplaySite;// A working copy of the site being edited
managedPropNames?: Array<string>; // the list of managed properties to be displayed
columns: Array<IColumn>; // the columns to show in the display
}
export class ManagedToCrawledMappingEntry {
constructor(
public crawledPropertyName: string,
public managedPropertyName: string,
) { }
}
export class DisplaySite {
/**
* Creates an instance of DisplaySite to be used in workingStorage when editing a site
* @param {string} Title
* @param {string} Url
* @param {string} SiteTemplate
* @param {Array<md.Message>} errorMessages
* @param {Array<DisplayProp>} [DisplayProps]
* @param {Array<string>} [searchableProps]
* @param {boolean} [forceCrawl]
*
* @memberOf DisplaySite
*/
constructor(
public Title: string,
public Url: string,
public SiteTemplate: string,
public errorMessages: Array<md.Message>,
public DisplayProps?: Array<DisplayProp>,
public searchableProps?: Array<string>,
public forceCrawl?: boolean,
) { }
}
export default class PropertyBagDisplay extends React.Component<IPropertyBagDisplayProps, IPropertyBagDisplayState> {
public constructor(props) {
super(props);
this.state = { sites: [], selectedIndex: -1, columns: [], errorMessages: [] };
}
/**Accessors */
/**
* Get's the commands to be displayed in the CommandBar. There is only one command (Edit).
* If no item is selected the command is disabled
*
*
* @readonly
* @type {Array<IContextualMenuItem>}
* @memberOf PropertyBagDisplay
*/
get CommandItems(): Array<IContextualMenuItem> {
return [
{
key: "Edit",
name: "Edit",
disabled: !(this.ItemIsSelected),
title: "Edit",
onClick: this.onEditItemClicked.bind(this),
icon: "Edit",
}];
};
get ItemIsSelected(): boolean {
if (!this.state) { return false; }
return (this.state.selectedIndex !== -1);
}
/** Utility Functions */
/**
* Renders the Panel used to edit a site's properties
*
* @returns
*
* @memberOf PropertyBagDisplay
*/
public renderPopup() {
if (!this.state.workingStorage) {
return (<div />);
}
else {
return (
<Panel
isOpen={this.state.isediting} type={PanelType.medium}
onDismiss={this.stopediting.bind(this)}
>
<MessageDisplay messages={this.state.workingStorage.errorMessages}
hideMessage={this.removePanelMessage.bind(this)} />
<div> <Label >Site Title</Label> {this.state.workingStorage.Title}</div>
<span> <Label label="" >Site Url</Label> {this.state.workingStorage.Url}</span>
<table>
<thead>
<tr>
<td>Managed Property Name</td>
<td>Value in Search Index</td>
<td>Crawled Property Name</td>
<td>Web Property Value</td>
<td>Searchable</td>
</tr>
</thead>
<tbody>
{this.state.workingStorage.DisplayProps.map((dp, i) => {
return (<tr>
<td>{dp.managedPropertyName}</td>
<td>{this.state.workingStorage[dp.managedPropertyName]}</td>
<td>{dp.crawledPropertyName}</td>
<td>
<TextField
data-crawledPropertyName={dp.crawledPropertyName}
value={dp.value}
onBlur={this.onPropertyValueChanged.bind(this)}
/>
</td>
<td>
<Toggle label=""
checked={dp.searchable}
onChanged={this.createSearcheableOnChangedHandler(dp.crawledPropertyName)}
/>
</td>
</tr>);
})}
</tbody>
</table>
<Toggle label="Force Crawl"
checked={this.state.workingStorage.forceCrawl}
onChanged={this.onForceCrawlChange.bind(this)}
/>
<Button default={true} icon="Save" buttonType={ButtonType.hero} value="Save" onClick={this.onSave.bind(this)} >Save</Button>
<Button icon="Cancel" buttonType={ButtonType.normal} value="Cancel" onClick={this.onCancel.bind(this)} >Cancel</Button>
</Panel>
);
}
}
/**
* Removes a message from the MessageDIsplay when the user click the 'x'
*
* @param {Array<md.Message>} messageList The list to remove the masseg from (the 'main' window of the Panel)
* @param {string} messageId The Id of the massge to remove
*
* @memberOf PropertyBagDisplay
*/
public removeMessage(messageList: Array<md.Message>, messageId: string) {
_.remove(messageList, {
Id: messageId
});
this.setState(this.state);
}
/**
* Removes a massage from the main windo
*
* @param {string} messageId
*
* @memberOf PropertyBagDisplay
*/
public removeMainMessage(messageId: string) {
this.removeMessage(this.state.errorMessages, messageId);
}
/**
* removes a message from the popup Panel
*
* @param {string} messageId
*
* @memberOf PropertyBagDisplay
*/
public removePanelMessage(messageId: string) {
this.removeMessage(this.state.workingStorage.errorMessages, messageId);
}
/**
* Makes the specified property either searchable or non-searchable in sharepoint
*
* @param {string} siteUrl The site to se it on
* @param {string} propname the managed property to set
* @param {boolean} newValue Searchable or not
* @returns {Promise<any>}
*
* @memberOf PropertyBagDisplay
*/
public changeSearchable(siteUrl: string, propname: string, newValue: boolean): Promise<any> {
if (newValue) {//make prop searchable
if (_.indexOf(this.state.workingStorage.searchableProps, propname) === -1) {// wasa not searchable, mpw it is
console.log(propname + "was not searchable, now it is ");
this.state.workingStorage.searchableProps.push(propname);
return utils.saveSearchablePropertiesToSharePoint(siteUrl, this.state.workingStorage.searchableProps);
}
else {
console.log(propname + "was not searchable, still is not ");
return Promise.resolve();
}
}
else { // make prop not searchablke
if (_.indexOf(this.state.workingStorage.searchableProps, propname) !== -1) {// wasa not searchable, mpw it is
console.log(propname + "was searchable, now it is not");
_.remove(this.state.workingStorage.searchableProps, p => { return p === propname; });
return utils.saveSearchablePropertiesToSharePoint(siteUrl, this.state.workingStorage.searchableProps);
}
else {
console.log(propname + "was searchable, still it is");
return Promise.resolve();
}
}
}
/**
* Switches component out of edit mode
*
*
* @memberOf PropertyBagDisplay
*/
public stopediting() {
this.state.isediting = false;
this.setState(this.state);
}
/**
* Caled by the Details list to render a column as a URL rather than text
*
* @private
* @param {*} [item]
* @param {number} [index]
* @param {IColumn} [column]
* @returns {*}
*
* @memberOf PropertyBagDisplay
*/
private renderSiteUrl(item?: any, index?: number, column?: IColumn): any {
return (<a href={item[column.fieldName]}>{item[column.fieldName]} </a>);
}
/**
* Sets the columns to be displayed in the list.
* These are SiteTemplate, Title and Url, plus any properties specified in
* the propertypane
*
* @private
* @returns {Array<IColumn>}
*
* @memberOf PropertyBagDisplay
*/
private setupColumns(): Array<IColumn> {
const columns: Array<IColumn> = [
{
fieldName: "SiteTemplate",
key: "SiteTemplate",
name: "SiteTemplate",
minWidth: 20,
maxWidth: 100,
},
{
fieldName: "Title",
key: "Title",
name: "Title",
minWidth: 20,
maxWidth: 220,
isSorted: false,
isSortedDescending: false
},
{
fieldName: "Url",
key: "Url",
name: "Url",
minWidth: 20,
maxWidth: 220,
onRender: this.renderSiteUrl
},
];
const displayProps: Array<string> = this.props.propertiesToDisplay.map(item => {
return item.split("|")[1];
});
for (const dp of displayProps) {
columns.push(
{
fieldName: dp,
key: dp,
name: dp,
minWidth: 20,
maxWidth: 220,
isSorted: false,
isSortedDescending: false
});
}
return columns;
}
/** react lifecycle */
/**
* Called when the componet loads.
* Builds the query to search sharepoint for the list of sites to display and formates
* the results to be displayed in the list
*
*
* @memberOf PropertyBagDisplay
*/
public componentWillMount() {
this.state.columns = this.setupColumns();
this.state.managedToCrawedMapping = [];
this.state.managedPropNames = [];
for (const prop of this.props.propertiesToDisplay) {
const names: Array<string> = prop.split('|');// crawledpropety/managed property
this.state.managedToCrawedMapping.push(new ManagedToCrawledMappingEntry(names[0], names[1]));
this.state.managedPropNames.push(names[1]);
}
this.state.managedPropNames.unshift("Title");
this.state.managedPropNames.unshift("Url");
this.state.managedPropNames.unshift("SiteTemplate");
this.state.managedPropNames.unshift("SiteTemplateId");
let querytext = "contentclass:STS_Site ";
if (this.props.siteTemplatesToInclude) {
if (this.props.siteTemplatesToInclude.length > 0) {
querytext += " AND (";
for (const siteTemplate of this.props.siteTemplatesToInclude) {
const siteTemplateParts = siteTemplate.split("#");
if (!siteTemplateParts[1]) {
querytext += "SiteTemplate=" + siteTemplateParts[0];
}
else {
querytext += "(SiteTemplate=" + siteTemplateParts[0] + " AND SiteTemplateId=" + siteTemplateParts[1] + ")";
}
if (this.props.siteTemplatesToInclude.indexOf(siteTemplate) !== this.props.siteTemplatesToInclude.length - 1)
{ querytext += " OR "; }
}
querytext += " )";
}
}
console.log("Using Query " + querytext);
const q: SearchQuery = {
Querytext: querytext,
SelectProperties: this.state.managedPropNames,
RowLimit: 999,
TrimDuplicates: false
};
pnp.sp.search(q).then((results: SearchResults) => {
for (const r of results.PrimarySearchResults) {
const obj: any = {};
for (const dp of this.state.managedPropNames) {
obj[dp] = r[dp];
}
obj.SiteTemplate = obj.SiteTemplate + "#" + obj.SiteTemplateId;
this.state.sites.push(obj);
}
debugger;
this.state.errorMessages.push(new md.Message("Items Recieved"));
this.setState(this.state);
}).catch(err => {
debugger;
this.state.errorMessages.push(new md.Message(err));
this.setState(this.state);
});
}
/** Event Handlers */
/**
* Changes the selected item
*
* @param {*} [item]
* @param {number} [index]
*
* @memberOf PropertyBagDisplay
*/
public onActiveItemChanged(item?: any, index?: number) {
this.state.selectedIndex = index;
this.setState(this.state);
}
/**
* Saves the item in workingStorage back to SharePoint
*
* @param {MouseEvent} [e]
*
* @memberOf PropertyBagDisplay
*/
public onSave(e?: MouseEvent): void {
const promises: Array<Promise<any>> = [];
for (const prop of this.state.workingStorage.DisplayProps) {
const promise = utils.setSPProperty(prop.crawledPropertyName, prop.value, this.state.workingStorage.Url)
.then(value => {
this.changeSearchable(this.state.workingStorage.Url, prop.crawledPropertyName, prop.searchable);
});
promises.push(promise);
}
Promise.all(promises)
.then((results: Array<any>) => {
if (this.state.workingStorage.forceCrawl) {
utils.forceCrawl(this.state.workingStorage.Url);
}
this.state.workingStorage = null;
this.state.isediting = false;
this.setState(this.state);
}).catch((err) => {
debugger;
this.state.workingStorage.errorMessages.push(new md.Message(err));
this.setState(this.state);
console.log(err);
});
}
/**
* Clears workingStorage and exits edit mode
*
* @param {MouseEvent} [e]
*
* @memberOf PropertyBagDisplay
*/
public onCancel(e?: MouseEvent): void {
this.state.isediting = false;
this.state.workingStorage = null;
this.setState(this.state);
}
/**
* Set the ForceCrawl Value in working storage which can be used to force a crawl of the site
* after the item is saved
*
* @param {boolean} newValue
*
* @memberOf PropertyBagDisplay
*/
public onForceCrawlChange(newValue: boolean) {
this.state.workingStorage.forceCrawl = newValue;
this.setState(this.state);
}
/**
* Called when the value of a property i schanged in the display.
* Saves the new value in workingStarage,
*
* @param {React.FormEvent<HTMLInputElement>} event
*
* @memberOf PropertyBagDisplay
*/
public onPropertyValueChanged(event: React.FormEvent<HTMLInputElement>) {
const selectedProperty = event.currentTarget.attributes["data-crawledpropertyname"].value;
const dp: DisplayProp = _.find(this.state.workingStorage.DisplayProps, p => { return p.crawledPropertyName === selectedProperty; });
dp.value = event.currentTarget.value;
this.setState(this.state);
}
public createSearcheableOnChangedHandler = (managedPropertyName) => (value) => {
const dp: DisplayProp = _.find(this.state.workingStorage.DisplayProps, p => { return p.crawledPropertyName === managedPropertyName; });
dp.searchable = value;
this.setState(this.state);
}
/**
* Called when user wishes to edit an item.
* The List displayes the values from the search index.
* This method gets the values from the actual PropertyBag so that they can be edited.
*
* @param {MouseEvent} [e]
*
* @memberOf PropertyBagDisplay
*/
public onEditItemClicked(e?: MouseEvent): void {
console.log("in onEditItemClicked");
const selectedSite = this.state.sites[this.state.selectedIndex];
const web = new Web(selectedSite.Url);
web.select("Title", "AllProperties").expand("AllProperties").get().then(r => {
const crawledProps: Array<string> = this.props.propertiesToDisplay.map(item => {
return item.split("|")[0];
});
this.state.workingStorage = _.clone(this.state.sites[this.state.selectedIndex]);
this.state.workingStorage.searchableProps = utils.decodeSearchableProps(r.AllProperties["vti_x005f_indexedpropertykeys"]);
this.state.workingStorage.DisplayProps = utils.SelectProperties(r.AllProperties, crawledProps, this.state.workingStorage.searchableProps);
this.state.workingStorage.errorMessages = new Array<md.Message>();
// now add in the managed Prop
for (const dp of this.state.workingStorage.DisplayProps) {
dp.managedPropertyName =
_.find(this.state.managedToCrawedMapping, mtc => { return mtc.crawledPropertyName === dp.crawledPropertyName; }).managedPropertyName;
}
this.state.isediting = true;
this.setState(this.state);
});
console.log("out onEditItemClicked");
}
/**
* Sorts a column when the user clicks on the header
*
* @private
* @param {*} event
* @param {IColumn} column
*
* @memberOf PropertyBagDisplay
*/
private _onColumnClick(event: any, column: IColumn) {
column = _.find(this.state.columns, c => c.fieldName === column.fieldName);// find the object in state
// If we've sorted this column, flip it.
if (column.isSorted) {
column.isSortedDescending = !column.isSortedDescending;
}
else {
column.isSorted = true;
column.isSortedDescending = false;
}
// Sort the items.
this.state.sites = _.orderBy(this.state.sites, [(site, x, y, z) => {
if (site[column.fieldName]) {
return site[column.fieldName].toLowerCase();
}
else {
return "";
}
}], [column.isSortedDescending ? "desc" : "asc"]);
this.setState(this.state);
}
/**
* Renders the component
*
* @returns {React.ReactElement<IPropertyBagDisplayProps>}
*
* @memberOf PropertyBagDisplay
*/
public render(): React.ReactElement<IPropertyBagDisplayProps> {
return (
<div >
<CommandBar items={this.CommandItems} />
<MessageDisplay
messages={this.state.errorMessages}
hideMessage={this.removeMainMessage.bind(this)}
/>
<DetailsList
key="Url"
onColumnHeaderClick={this._onColumnClick.bind(this)}
items={this.state.sites}
layoutMode={DetailsListLayoutMode.fixedColumns}
columns={this.state.columns}
selectionMode={SelectionMode.single}
checkboxVisibility={CheckboxVisibility.hidden}
onActiveItemChanged={this.onActiveItemChanged.bind(this)
}
>
</DetailsList>
{this.renderPopup.bind(this)()}
</div >
);
}
}

View File

@ -0,0 +1,9 @@
define([], function () {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"PropertiesToDisplayFieldLabel": "Properties to Display",
"SiteTemplatesToIncludeFieldLabel": "Site Templates to Inculled (STS#1, or just STS for all)"
}
});

View File

@ -0,0 +1,12 @@
declare interface IPropertyBagDisplayStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
PropertiesToDisplayFieldLabel:string;
SiteTemplatesToIncludeFieldLabel:string;
}
declare module 'propertyBagDisplayStrings' {
const strings: IPropertyBagDisplayStrings;
export = strings;
}

View File

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

View File

@ -0,0 +1,6 @@
export interface IPropertyBagEditorWebPartProps {
description: string;
propertiesToEdit:string; //separated by \n
}

View File

@ -0,0 +1,22 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "f3ac8a07-2a9b-47a1-8a7e-a093cad63f98",
"alias": "PropertyBagEditorWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [{
"groupId": "f3ac8a07-2a9b-47a1-8a7e-a093cad63f98",
"group": { "default": "Property Bag Navigation" },
"title": { "default": "Property Bag Editor" },
"description": { "default": "Lets you edit the properties of an SPSite passed in as a query parameter" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "PropertyBagEditor",
"propertiesToEdit":
"CUSTOMNAVAreaName\nCUSTOMNAVBusinessUnit\nCUSTOMNAVContinent\nCUSTOMNAVLocation"
}
}]
}

View File

@ -0,0 +1,88 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version, UrlQueryParameterCollection } from "@microsoft/sp-core-library";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from "@microsoft/sp-webpart-base";
import * as strings from "propertyBagEditorStrings";
import PropertyBagEditor from "./components/PropertyBagEditor";
import { IPropertyBagEditorProps } from "./components/IPropertyBagEditorProps";
import { IPropertyBagEditorWebPartProps } from "./IPropertyBagEditorWebPartProps";
import utils from "../shared/utils";
/**
* This webpart is used to edit the properties of a Rootweb.
* The web to be edited is passed in by the SiteUrl Property. If not set, the Current site is used
* @export
* @class PropertyBagEditorWebPart
* @extends {BaseClientSideWebPart<IPropertyBagEditorWebPartProps>}
*/
export default class PropertyBagEditorWebPart extends BaseClientSideWebPart<IPropertyBagEditorWebPartProps> {
/**
* Renders the component. If no siteUrl is present on the currnt url, the current site is
* passed in as the siteUrl Property.
*
* converts the propertiesToEdit from a new-line (\n) separated string to an array of
* strings to be passed to the component.
*
*
* @memberOf PropertyBagEditorWebPart
*/
public render(): void {
const uqpc = new UrlQueryParameterCollection(window.location.toString());
let siteUrl: string = uqpc.getValue("siteUrl");
if (!siteUrl) {
siteUrl = this.context.pageContext.site.absoluteUrl;
}
const props: IPropertyBagEditorProps = {
description: this.properties.description,
propertiesToEdit: utils.parseMultilineTextToArray(this.properties.propertiesToEdit),
siteUrl: siteUrl
};
const element: React.ReactElement<IPropertyBagEditorProps> = React.createElement(
PropertyBagEditor,
props
);
ReactDom.render(element, 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("description", {
label: strings.DescriptionFieldLabel
}),
PropertyPaneTextField("propertiesToEdit", {
label: strings.PropertiesToEditFieldLabel,
multiline: true
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,5 @@
export interface IPropertyBagEditorProps {
description: string;
propertiesToEdit:Array<string>;
siteUrl:string;
}

View File

@ -0,0 +1,23 @@
.helloWorld {
.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 {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
text-decoration: none;
}
}

View File

@ -0,0 +1,256 @@
import * as React from "react";
import { css } from "office-ui-fabric-react"; import { IPropertyBagEditorProps } from "./IPropertyBagEditorProps";
import { Web } from "sp-pnp-js";
import * as _ from "lodash";
import utils from "../../shared/utils";
require("sp-init");
require("microsoft-ajax");
require("sp-runtime");
require("sharepoint");
import { IContextualMenuItem, } from "office-ui-fabric-react/lib/ContextualMenu";
import { CommandBar } from "office-ui-fabric-react/lib/CommandBar";
import { MessageBar } from "office-ui-fabric-react/lib/MessageBar";
import {
DetailsList, DetailsListLayoutMode, IColumn, SelectionMode, CheckboxVisibility,
} from "office-ui-fabric-react/lib/DetailsList";
import { Dialog, DialogType } from "office-ui-fabric-react/lib/Dialog";
import { Label } from "office-ui-fabric-react/lib/Label";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Toggle } from "office-ui-fabric-react/lib/Toggle";
import { Button, ButtonType } from "office-ui-fabric-react/lib/Button";
import DisplayProp from "../../shared/DisplayProp";
export interface IPropertyBagEditorState {
displayProps: Array<DisplayProp>; // The list of properties displayed in the webpart
workingStorage?: DisplayProp; // a working copy of the property currently being edited
selectedIndex: number; // the index of the currently selected propety
searchableProps: Array<string>; // an array of all the searchable properties in the site
messsage: string; // an error message
isediting: boolean; // whether the webart is in enit mode
}
export default class PropertyBagEditor extends React.Component<IPropertyBagEditorProps, IPropertyBagEditorState> {
public refs: {
[key: string]: React.ReactInstance;
list: DetailsList
};
public constructor(props: IPropertyBagEditorProps) {
super(props);
this.state = { searchableProps: [], displayProps: [], selectedIndex: -1, messsage: "", isediting: false };
}
/**Accessors */
/**
* Get's the commands to be displayed in the CommandBar. There is only one command (Edit).
* If no item is selected the command is disabled
*
* @readonly
* @type {Array<IContextualMenuItem>}
* @memberOf PropertyBagEditor
*/
get CommandItems(): Array<IContextualMenuItem> {
return [
{
key: "a",
name: "Edit",
disabled: !(this.ItemIsSelected),
title: "Edit",
onClick: this.onEditItemClicked.bind(this),
icon: "Edit"
}];
};
/**
* Determines if an item is selected.
*
* @readonly
* @type {boolean}
* @memberOf PropertyBagEditor
*/
get ItemIsSelected(): boolean {
if (!this.state) { return false; }
return (this.state.selectedIndex != -1);
}
/** react lifecycle */
public componentWillMount() {
const web = new Web(this.props.siteUrl);
web.select("Title", "AllProperties").expand("AllProperties").get().then(r => {
const sp = utils.decodeSearchableProps(r.AllProperties["vti_x005f_indexedpropertykeys"]);
const dp = utils.SelectProperties(r.AllProperties, this.props.propertiesToEdit, sp);
this.state.searchableProps = sp;
this.state.displayProps = dp;
this.setState(this.state);
});
}
/** event hadlers */
public stopediting() {
this.state.isediting = false;
this.setState(this.state);
}
public onActiveItemChanged(item?: any, index?: number) {
this.state.selectedIndex = index;
this.setState(this.state);
}
/**
* Gets fired when the user changes the 'Searchable' value in the ui.
* Saves the value in workingStorage
*
* @param {boolean} newValue
*
* @memberOf PropertyBagEditor
*/
public onSearchableValueChanged(newValue: boolean) {
this.state.workingStorage.searchable = newValue;
this.setState(this.state);
}
/**
* Gets fired when the user changes the proprty value in the ui
* Saves the value in workingStorage
*
* @param {any} event
*
* @memberOf PropertyBagEditor
*/
public onPropertyValueChanged(event) {
this.state.workingStorage.value = event.target.value;
this.setState(this.state);
}
/**
* Copies the selected item into workingStorage and sets the webpart into edit mode.
*
* @param {MouseEvent} [e]
*
* @memberOf PropertyBagEditor
*/
public onEditItemClicked(e?: MouseEvent): void {
this.state.isediting = true;
this.state.workingStorage = _.clone(this.state.displayProps[this.state.selectedIndex]);
this.setState(this.state);
}
/**
* Saves the item in workingStorage back to sharepoint, then clears workingStorage and stops editing.
*
* @param {MouseEvent} [e]
*
* @memberOf PropertyBagEditor
*/
public onSave(e?: MouseEvent): void {
utils.setSPProperty(this.state.workingStorage.crawledPropertyName, this.state.workingStorage.value, this.props.siteUrl)
.then(value => {
this.changeSearchable(this.state.workingStorage.crawledPropertyName, this.state.workingStorage.searchable)
.then(s => {
this.state.displayProps[this.state.selectedIndex] = this.state.workingStorage;
this.state.workingStorage = null;
this.state.isediting = false;
this.setState(this.state);
});
});
}
/**
* Clears workingStorage and stops editing
*
* @param {MouseEvent} [e]
*
* @memberOf PropertyBagEditor
*/
public onCancel(e?: MouseEvent): void {
this.state.isediting = false;
this.state.workingStorage = null;
this.setState(this.state);
}
/**
* Makes a propety Searchable or non-Searchable in the sharepoint site
*
* @param {string} propname The property to be made Searchable or non-Searchable
* @param {boolean} newValue Whether to make it Searchable or non-Searchable
* @returns {Promise<any>}
*
* @memberOf PropertyBagEditor
*/
public changeSearchable(propname: string, newValue: boolean): Promise<any> {
if (newValue) {//make prop searchable
if (_.indexOf(this.state.searchableProps, propname) === -1) {// wasa not searchable, mpw it is
this.state.searchableProps.push(propname);
return utils.saveSearchablePropertiesToSharePoint(this.props.siteUrl, this.state.searchableProps);
}
else {
return Promise.resolve();
}
}
else { // make prop not searchablke
if (_.indexOf(this.state.searchableProps, propname) !== -1) {// wasa not searchable, mpw it is
_.remove(this.state.searchableProps, p => { return p === propname; });
return utils.saveSearchablePropertiesToSharePoint(this.props.siteUrl, this.state.searchableProps);
}
else {
return Promise.resolve();
}
}
}
private RenderBoolean(item?: any, index?: number, column?: IColumn): any {
if (item[column.fieldName]) {
return (<div>Yes</div>);
} else {
return (<div>No</div>);
}
}
/**
* Renders the webpart
*
* @returns {React.ReactElement<IPropertyBagEditorProps>}
*
* @memberOf PropertyBagEditor
*/
public render(): React.ReactElement<IPropertyBagEditorProps> {
const columns: Array<IColumn> = [
{ isResizable: true, key: "name", name: "Propert Name", fieldName: "crawledPropertyName", minWidth: 150 },
{ isResizable: true, key: "value", name: "Propert Value", fieldName: "value", minWidth: 150 },
{ key: "searchable", name: "searchable", fieldName: "searchable", minWidth: 150, onRender: this.RenderBoolean },
];
return (
<div>
<CommandBar items={this.CommandItems} />
<DetailsList layoutMode={DetailsListLayoutMode.fixedColumns}
columns={columns}
selectionMode={SelectionMode.single}
checkboxVisibility={CheckboxVisibility.hidden}
items={this.state.displayProps}
onActiveItemChanged={this.onActiveItemChanged.bind(this)}
>
</DetailsList>
<Dialog
isOpen={this.state.isediting} type={DialogType.close}
onDismiss={this.stopediting.bind(this)}
title={(this.state.workingStorage) ? this.state.workingStorage.crawledPropertyName : ""} >
<span> <Label>Site Url</Label> {this.props.siteUrl}</span>
<TextField
value={(this.state.workingStorage) ? this.state.workingStorage.value : ""}
onBlur={this.onPropertyValueChanged.bind(this)}
/>
<Toggle label="Searchable"
checked={(this.state.workingStorage) ? this.state.workingStorage.searchable : undefined}
onChanged={this.onSearchableValueChanged.bind(this)}
/>
<Button default={true} icon="Save" buttonType={ButtonType.icon} value="Save" onClick={this.onSave.bind(this)} >Save</Button>
<Button icon="Cancel" buttonType={ButtonType.icon} value="Cancel" onClick={this.onCancel.bind(this)} >Cancel</Button>
</Dialog>
</div>
);
}
}

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"PropertiesToEditFieldLabel":"Properties to Edit"
}
});

View File

@ -0,0 +1,11 @@
declare interface IPropertyBagEditorStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
PropertiesToEditFieldLabel:string;
}
declare module 'propertyBagEditorStrings' {
const strings: IPropertyBagEditorStrings;
export = strings;
}

View File

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

View File

@ -0,0 +1,9 @@
export interface IPropertyBagFilteredSiteListWebPartProps {
description: string;
siteTemplatesToInclude: string; //STS#1 STS#2 separated by \n leave off the #1 to get all STS
filters: string; // managedPropertyname=valiust separated by \n (new line)
userFilters: string; // managedPropertyname=valiust separated by \n (new line)// Lets user filters
showSiteDescriptions: boolean;
linkTarget: string;// open sites in new window?
showQueryText: boolean; // display querytext in a messagegDisplay(Used for debugging)
}

View File

@ -0,0 +1,27 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "b81a6789-e93b-4be5-baa7-59f34004694a",
"alias": "PropertyBagFilteredSiteListWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [
{
"groupId": "b81a6789-e93b-4be5-baa7-59f34004694a",
"group": { "default": "Property Bag Navigation" },
"title": { "default": "Property Bag Site List" },
"description": {
"default": "Displays a list of sites with the selected properties"
},
"officeFabricIconFontName": "Page",
"properties": {
"description": "Project Sites",
"filters": "AreaName=Projects",
"siteTemplatesToInclude": "STS",
"userFilters": "Continent",
"linkTarget": "_self",
"showSiteDescriptions": true
}
}
]
}

View File

@ -0,0 +1,106 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneToggle, PropertyPaneChoiceGroup
} from '@microsoft/sp-webpart-base';
import * as strings from 'propertyBagFilteredSiteListStrings';
import PropertyBagFilteredSiteList from './components/PropertyBagFilteredSiteList';
import { IPropertyBagFilteredSiteListProps } from './components/IPropertyBagFilteredSiteListProps';
import { IPropertyBagFilteredSiteListWebPartProps } from './IPropertyBagFilteredSiteListWebPartProps';
import utils from "../shared/utils";
export default class PropertyBagFilteredSiteListWebPart extends BaseClientSideWebPart<IPropertyBagFilteredSiteListWebPartProps> {
/**
* Renders the component.
*
* converts the new-line (\n) separated strings to an array of
* strings to be passed to the component.
*
*
*
* @memberOf PropertyBagFilteredSiteListWebPart
*/
public render(): void {
debugger;
const element: React.ReactElement<IPropertyBagFilteredSiteListProps> = React.createElement(
PropertyBagFilteredSiteList,
{
description: this.properties.description,
siteTemplatesToInclude: utils.parseMultilineTextToArray(this.properties.siteTemplatesToInclude),
filters: utils.parseMultilineTextToArray(this.properties.filters),
userFilters: utils.parseMultilineTextToArray(this.properties.userFilters),
showSiteDescriptions: this.properties.showSiteDescriptions,
linkTarget: this.properties.linkTarget,
showQueryText: this.properties.showQueryText
}
);
ReactDom.render(element, 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('description', {
label: strings.DescriptionFieldLabel
}),
,
PropertyPaneTextField("filters", {
label: strings.FiltersFieldLabel,
description: strings.FiltersFieldDescription,
multiline: true,
resizable: true
}),
PropertyPaneTextField("siteTemplatesToInclude", {
label: strings.SiteTemplatesToIncludeFieldLabel,
description: strings.SiteTemplatesToIncludeFieldDescription,
multiline: true,
resizable: true
}),
PropertyPaneTextField("userFilters", {
label: strings.UserFiltersFieldLabel,
description: strings.UserFiltersFieldDescription,
multiline: true,
resizable: true
}),
PropertyPaneChoiceGroup("linkTarget", {
label: strings.LinkTargetFieldLabel,
options: [
{ text: strings.TargetBlankDescription, key: "_blank" },
{ text: strings.TargetSelfDescription, key: "_self" },
{ text: strings.TargetParentDescription, key: "_parent" },
{ text: strings.TargetTopDescription, key: "_top" },
],
}),
PropertyPaneToggle("showSiteDescriptions", {
label: strings.ShowSiteDescriptionsFieldLabel,
}),
PropertyPaneToggle("showQueryText", {
label: strings.ShowQueryTextFieldLabel
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,10 @@
export interface IPropertyBagFilteredSiteListProps {
description: string;
filters: Array<string>; // ManagedProprtyName=value , delimeted by \n
siteTemplatesToInclude: Array<string>;// STS#0 delimeted by \n
userFilters: Array<string>; // managedPropertyname=valiust separated by \n (new line)// Lets user filters
showSiteDescriptions: boolean;
linkTarget: string;
showQueryText: boolean;
}

View File

@ -0,0 +1,23 @@
.helloWorld {
.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 {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
text-decoration: none;
}
}

View File

@ -0,0 +1,441 @@
import * as React from "react";
import pnp from "sp-pnp-js";
import { SortDirection } from "sp-pnp-js";
import * as _ from "lodash";
import DisplayProp from "../../shared/DisplayProp";
import { SearchQuery, SearchResults } from "sp-pnp-js";
import { css } from "office-ui-fabric-react";
//import styles from "./PropertyBagFilteredSiteList.module.scss";
import { IPropertyBagFilteredSiteListProps } from "./IPropertyBagFilteredSiteListProps";
import { Label } from "office-ui-fabric-react/lib/Label";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { List } from "office-ui-fabric-react/lib/List";
import { Button, ButtonType } from "office-ui-fabric-react/lib/Button";
import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar";
import * as md from "../../shared/MessageDisplay";
import utils from "../../shared/utils";
import MessageDisplay from "../../shared/MessageDisplay";
import { CommandBar, ICommandBarProps } from "office-ui-fabric-react/lib/CommandBar";
import {
DetailsList, DetailsListLayoutMode, IColumn, IGroupedList, SelectionMode, CheckboxVisibility, IGroup
} from "office-ui-fabric-react/lib/DetailsList";
import {
GroupedList
} from "office-ui-fabric-react/lib/GroupedList";
import {
IViewport
} from "office-ui-fabric-react/lib/utilities/decorators/withViewport";
import {
Panel, PanelType
} from "office-ui-fabric-react/lib/Panel";
import { IContextualMenuItem, } from "office-ui-fabric-react/lib/ContextualMenu";
export interface IPropertyBagFilteredSiteListState {
errorMessages: Array<md.Message>;
sites: Array<Site>;
filteredSites: Array<Site>;
userFilters: Array<UserFilter>;// this is what the user CAN filter on
appliedUserFilters: Array<AppliedUserFilter>;// this is what the user HAS filtered on
}
export class Site {
public constructor(
public title: string,
public description: string,
public url: string,
) { }
}
export class DisplaySite {
constructor(
public Title: string,
public Url: string,
public SiteTemplate: string,
public errorMessages: Array<md.Message>,
public DisplayProps?: Array<DisplayProp>,
public searchableProps?: Array<string>,
public forceCrawl?: boolean,
) { }
}
export class AppliedUserFilter {
/**
* Creates an instance of AppliedUserFilter.
* An AppliedUserFilter is created when a user oerforms a filtering operation
* @param {string} managedPropertyName The property the user filtered on
* @param {string} value The value the user selected for the filter
*
* @memberOf AppliedUserFilter
*/
public constructor(
public managedPropertyName: string,
public value: string)
{ }
}
export class UserFilter {
/**
* A UserFilter lets the use filter the list of displayed sites based on a metadata value.
* The ManagedProperty name is displayed in a CommandBar as a dropdown, with the values as
* dropdown options.
*
* @type {Array<string>}
* @memberOf UserFilter
*/
public values: Array<string>;
public constructor(public managedPropertyName: string) {
this.values = [];
}
}
export default class PropertyBagFilteredSiteList extends React.Component<IPropertyBagFilteredSiteListProps, IPropertyBagFilteredSiteListState> {
public constructor(props) {
console.log(JSON.stringify("in constructor"));
super(props);
this.state = { sites: [], filteredSites: [], errorMessages: [], userFilters: [], appliedUserFilters: [] };
}
/** Utility Functions */
/**
* Removes a message from the MessageDisplay
*
* @param {string} messageId the ID of the message to remove
*
* @memberOf PropertyBagFilteredSiteList
*/
public removeMessage(messageId: string) {
_.remove(this.state.errorMessages, {
Id: messageId
});
this.setState(this.state);
}
/**
* Initializes the list of user filters.
* A user filter is created for each UserFilter name specified in the props.
*
* @param {Array<string>} userFilterNames
*
* @memberOf PropertyBagFilteredSiteList
*/
public setupUserFilters(userFilterNames: Array<string>) {
console.log(JSON.stringify("in extractUserFilterValues"));
this.state.userFilters = [];
for (const userFilterName of userFilterNames) {
this.state.userFilters.push(new UserFilter(userFilterName));
}
}
/**
* Adds values to All the UserFilters for a given SearchResults.
*
* @param {any} r The Searchresult
*
* @memberOf PropertyBagFilteredSiteList
*/
public extractUserFilterValues(r) {
console.log(JSON.stringify("in extractUserFilterValues"));
for (const userFilter of this.state.userFilters) {
const value = r[userFilter.managedPropertyName].trim();
if (_.find(userFilter.values, v => { return v === value; })) {
// already there
}
else {
userFilter.values.push(value);
}
}
}
/**
* Gets the sites to be displayed in the list using the filters passed in from Properies
* Sites are saved in this.state.sites
* @param {Array<string>} siteTemplatesToInclude Site templats (i.e. STS of STS#0, etc,)
* @param {Array<string>} filters Filters to use from PropertyPane
* @param {boolean} showQueryText Whether to display the queryText in the MessageDisplay
* @param {Array<string>} userFilters the list of user filters to be built from the searchresults
* @param {boolean} showSiteDescriptions Include site descroptions in search results
*
* @memberOf PropertyBagFilteredSiteList
*/
public getSites(siteTemplatesToInclude: Array<string>, filters:Array<string>, showQueryText: boolean, userFilters: Array<string>, showSiteDescriptions: boolean) {
console.log(JSON.stringify("in getSites"));
const userFilterNameArray = [];
if (userFilters) {
for (const userFilter of userFilters) {
userFilterNameArray.push(userFilter);
}
}
let querytext = "contentclass:STS_Site ";
if (siteTemplatesToInclude) {
querytext = utils.addSiteTemplatesToSearchQuery(siteTemplatesToInclude, querytext);
}
if (filters) {
querytext = utils.addFiltersToSearchQuery(filters, querytext);
}
if (showQueryText) {
this.state.errorMessages.push(new md.Message("Using Query " + querytext));
}
const selectProperties: Array<string> = ["Title", "SPSiteUrl"];
if (showSiteDescriptions) {
selectProperties.push("Description");
}
for (const userFilter of userFilterNameArray) {
selectProperties.push(userFilter);
}
const q: SearchQuery = {
Querytext: querytext,
SelectProperties: selectProperties,
RowLimit: 999,
TrimDuplicates: false,
// SortList:
// [
// {
// Property: 'Title',
// Direction: SortDirection.Ascending
// }
// ]
};
pnp.sp.search(q).then((results: SearchResults) => {
this.state.sites = [];
debugger;
this.setupUserFilters(userFilterNameArray);
for (const r of results.PrimarySearchResults) {
const index = this.state.sites.push(new Site(r.Title, r.Description, r.SPSiteUrl));
for (const mp of this.props.userFilters) {
this.state.sites[index-1][mp] = r[mp];
}
this.extractUserFilterValues(r);
}
this.filterSites();
this.setState(this.state);
}).catch(err => {
debugger;
this.state.errorMessages.push(new md.Message(err));
this.setState(this.state);
});
}
/** react lifecycle */
/**
* Called whe component loads.
* Gets the sites and builds the userFilters
*
* @memberOf PropertyBagFilteredSiteList
*/
public componentWillMount() {
console.log(JSON.stringify("in componentWillMount"));
this.getSites(this.props.siteTemplatesToInclude, this.props.filters, this.props.showQueryText, this.props.userFilters, this.props.showSiteDescriptions);
}
/**
* Called whe Properties are changed in the PropertyPane
* Gets the sites and builds the userFilters using the new Properties
*
* @param {IPropertyBagFilteredSiteListProps} nextProps
* @param {*} nextContext
*
* @memberOf PropertyBagFilteredSiteList
*/
public componentWillReceiveProps(nextProps: IPropertyBagFilteredSiteListProps, nextContext: any) {
console.log(JSON.stringify("in componentWillReceiveProps"));
this.getSites(nextProps.siteTemplatesToInclude, nextProps.filters, nextProps.showQueryText, nextProps.userFilters, nextProps.showSiteDescriptions);
}
/**
* Called by the Render method.
* Displayes the Site Description if requested in the PropertyPane.
* Otherwise displays an empty Div
*
* @param {Site} site
* @returns
*
* @memberOf PropertyBagFilteredSiteList
*/
public conditionallyRenderDescription(site: Site) {
console.log(JSON.stringify("in conditionallyRenderDescription"));
if (this.props.showSiteDescriptions) {
return (<Label>{site.description}</Label>);
}
else {
return (<div />);
}
}
/**
* Called by the Render Method
* Sets up the ContentualMenuItems based on the FilterData extracted from the SearchResults
*
* @private
* @returns {Array<IContextualMenuItem>}
*
* @memberOf PropertyBagFilteredSiteList
*/
private SetupFilters(): Array<IContextualMenuItem> {
console.log(JSON.stringify("in SetupFilters"));
const items = new Array<IContextualMenuItem>();
for (const uf of this.state.userFilters) {
const item: IContextualMenuItem = {
key: uf.managedPropertyName,
name: uf.managedPropertyName,
title: uf.managedPropertyName,
href:null,
}
item.items = [];
for (const value of uf.values) {
item.items.push({
key: value,
data: {
managedPropertyName: uf.managedPropertyName,
value: value
},
checked: this.AppliedFilterExists(uf.managedPropertyName, value),
name: value,
title: value,
onClick: this.filterOnMetadata.bind(this)
});
}
items.push(item);
}
return items;
}
/**
* Determines if the specified managedProperty and value are currently being filtered on
*
* @param {string} managedPropertyName
* @param {string} value
* @returns {boolean}
*
* @memberOf PropertyBagFilteredSiteList
*/
public AppliedFilterExists(managedPropertyName: string, value: string): boolean {
console.log(JSON.stringify("in AppliedFilterExists"));
const selectedFilter = _.find(this.state.appliedUserFilters, af => {
return (af.managedPropertyName === managedPropertyName && af.value === value);
});
if (selectedFilter) {
return true;
} else {
return false;
}
}
/**
* Togles the userFIlter fpr the managedProperty and value in the specified MenuItem
*
* @param {IContextualMenuItem} item
*
* @memberOf PropertyBagFilteredSiteList
*/
public ToggleAppliedUserFilter(item: IContextualMenuItem) {
console.log(JSON.stringify("in ToggleAppliedUserFilter"));
if (this.AppliedFilterExists(item.data.managedPropertyName, item.data.value)) {
this.state.appliedUserFilters = this.state.appliedUserFilters.filter(af => {
return (af.managedPropertyName !== item.data.managedPropertyName || af.value !== item.data.value);
});
}
else {
this.state.appliedUserFilters.push(new AppliedUserFilter(item.data.managedPropertyName, item.data.value));
}
}
/**
* Filters the sites in this.state.sites using the userFilteres in this.state.appliedUserFilters
* and stores it in this.state.filteredSites.
* this.state.sites holds all sites after filtering based on propertypane filters.
* this.state.filteredSites hods the likst of sites after userFilters are applied and
* is shown in the display
*
* @memberOf PropertyBagFilteredSiteList
*/
public filterSites() {
console.log(JSON.stringify("in filterSites"));
if (this.state.appliedUserFilters.length === 0) {
this.state.filteredSites = this.state.sites;
}
else {
this.state.filteredSites = this.state.sites.filter(site => {
debugger;
for (const auf of this.state.appliedUserFilters) {
if (site[auf.managedPropertyName] !== auf.value) {
return false;
}
}
return true;
});
}
}
/**
* EventHandler called when a user selects one of the filters from the COmmandBar
* Toggles the filter
* Applies the new Filters.
* re-deiplays the list
*
* @param {React.MouseEvent<HTMLElement>} [ev]
* @param {IContextualMenuItem} [item]
*
* @memberOf PropertyBagFilteredSiteList
*/
public filterOnMetadata(ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) {
console.log(JSON.stringify("in filterOnMetadata"));
this.ToggleAppliedUserFilter(item);
this.filterSites();
this.setState(this.state);
}
public doNothing(ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) {
console.log(JSON.stringify("in doNothing"));
ev.stopPropagation();
return false;
}
/**
* Renders the list of sites in this.state.filteredSites.
*
* @returns {React.ReactElement<IPropertyBagFilteredSiteListProps>}
*
* @memberOf PropertyBagFilteredSiteList
*/
public render(): React.ReactElement<IPropertyBagFilteredSiteListProps> {
debugger;
const listItems = this.state.filteredSites.map((site) =>
<li >
<a href={site.url} target={this.props.linkTarget}>{site.title}</a>
{this.conditionallyRenderDescription(site)}
</li>
);
const commandItems: Array<IContextualMenuItem> = this.SetupFilters();
console.log(JSON.stringify("commandItems follow"));
console.log(JSON.stringify(commandItems));
return (
<div >
<Label>{this.props.description}</Label>
<CommandBar items={commandItems} />
<MessageDisplay
messages={this.state.errorMessages}
hideMessage={this.removeMessage.bind(this)}
/>
<ul > {listItems}</ul>
{/*<List items={sites} startIndex={0}
onRenderCell={(site, index) => {
return (
<div >
<Link href={site.url}>{site.title}</Link>
<Label>{site.description}</Label>
</div>
);
}}
>
</List>*/}
</div >
);
}
}

View File

@ -0,0 +1,21 @@
define([], function () {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"SiteTemplatesToIncludeFieldLabel": "Site Templates to Include",
"FiltersFieldLabel": "Metadata Filters",
"FiltersFieldDescription": "A list of filters in the form 'ManagedPropertyName=value' used to filter thhe sites shjown in the list",
"LinkTargetFieldLabel": "Target for Opnning links",
"LinkTargetFieldDescription": "Target for Opnning links",
"ShowSiteDescriptionsFieldLabel": "Show Site Descriptions",
"SiteTemplatesToIncludeFieldDescription": "A list of SiteTemplate names and IDs separated by a '#' characher Leave off the SiteTemplate ID to iclude All. (e.g STS#1 will include Teamsites, STS will include All STS Sites)",
"UserFiltersFieldLabel": "User Filters",
"UserFiltersFieldDescription": "A list of Managed Property Names that will let the user further filter the list of sites. ",
"ShowQueryTextFieldLabel": "Show Query Text",
"TargetBlankDescription": "Open Site in a new window or tab (_blank)",
"TargetSelfDescription": "Open Site in the same frame as it was clicked (_self)",
"TargetParentDescription": "Open Site in the parent frame (_parent}",
"TargetTopDescription": "Open Site in the full body of the window (_top)"
}
});

View File

@ -0,0 +1,23 @@
declare interface IPropertyBagFilteredSiteListStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
SiteTemplatesToIncludeFieldLabel: string;
SiteTemplatesToIncludeFieldDescription: string;
FiltersFieldLabel: string;
FiltersFieldDescription: string;
LinkTargetFieldLabel: string;
ShowSiteDescriptionsFieldLabel: string;
UserFiltersFieldLabel: string;
UserFiltersFieldDescription: string;
ShowQueryTextFieldLabel: string;
TargetBlankDescription: string;
TargetSelfDescription: string;
TargetParentDescription: string;
TargetTopDescription: string;
}
declare module 'propertyBagFilteredSiteListStrings' {
const strings: IPropertyBagFilteredSiteListStrings;
export = strings;
}

View File

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

View File

@ -0,0 +1,6 @@
export interface IPropertyBagGlobalNavWebPartProps {
description: string;
siteTemplatesToInclude: string; //STS#1 STS#2 separated by \n leave off the #1 to get all STS
filters: string; // managedPropertyname=valiust separated by \n (new line)
managedProperties:string;// managed properties to build the menu from.
}

View File

@ -0,0 +1,28 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "8634e32b-eda4-483d-8fe9-5f2075339eb8",
"alias": "PropertyBagGlobalNavWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [
{
"groupId": "8634e32b-eda4-483d-8fe9-5f2075339eb8",
"group": {
"default": "Property Bag Navigation"
},
"title": {
"default": "Property Bag NavBar"
},
"description": {
"default": "Displays a Global Navigation Menu built from the Sites PropertyBags"
},
"officeFabricIconFontName": "Page",
"properties": {
"description": "propertyBagGlobalNav",
"filters": "",
"managedProperties": "Continent\nLocaltion"
}
}
]
}

View File

@ -0,0 +1,75 @@
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 utils from "../shared/utils";
import * as strings from 'propertyBagGlobalNavStrings';
import PropertyBagGlobalNav from './components/PropertyBagGlobalNav';
import { IPropertyBagGlobalNavProps } from './components/IPropertyBagGlobalNavProps';
import { IPropertyBagGlobalNavWebPartProps } from './IPropertyBagGlobalNavWebPartProps';
export default class PropertyBagGlobalNavWebPart extends BaseClientSideWebPart<IPropertyBagGlobalNavWebPartProps> {
public render(): void {
const element: React.ReactElement<IPropertyBagGlobalNavProps> = React.createElement(
PropertyBagGlobalNav,
{
description: this.properties.description,
managedProperties: utils.parseMultilineTextToArray( this.properties.managedProperties),
siteTemplatesToInclude: utils.parseMultilineTextToArray(this.properties.siteTemplatesToInclude),
filters: utils.parseMultilineTextToArray(this.properties.filters),
}
);
ReactDom.render(element, 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('description', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneTextField("filters", {
label: strings.FiltersFieldLabel,
description: strings.FiltersFieldDescription,
multiline: true,
resizable: true
}),
PropertyPaneTextField("siteTemplatesToInclude", {
label: strings.SiteTemplatesToIncludeFieldLabel,
description: strings.SiteTemplatesToIncludeFieldDescription,
multiline: true,
resizable: true
}),
PropertyPaneTextField("managedProperties", {
label: strings.ManagedPropertiesFieldLabel,
description: strings.ManagedPropertiesFieldDescription,
multiline: true,
resizable: true
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
export interface IPropertyBagGlobalNavProps {
description: string;
siteTemplatesToInclude: Array<string>; //STS#1 STS#2 leave off the #1 to get all STS
filters: Array<string>; // managedPropertyname=valiust separated by \n (new line)
managedProperties: Array<string>;// managed properties to build the menu from.
}

View File

@ -0,0 +1,52 @@
.helloWorld {
.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 {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: #0078d7;
border-color: #0078d7;
color: #ffffff;
// 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: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,136 @@
import * as React from 'react';
import { IPropertyBagGlobalNavProps } from './IPropertyBagGlobalNavProps';
import pnp from "sp-pnp-js";
import { SortDirection } from "sp-pnp-js";
import * as _ from "lodash";
import { SearchQuery, SearchResults } from "sp-pnp-js";
import { css } from "office-ui-fabric-react";
import { Label } from "office-ui-fabric-react/lib/Label";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { List } from "office-ui-fabric-react/lib/List";
import { Button, ButtonType } from "office-ui-fabric-react/lib/Button";
import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar";
import * as md from "../../shared/MessageDisplay";
import utils from "../../shared/utils";
import { CommandBar, ICommandBarProps } from "office-ui-fabric-react/lib/CommandBar";
import {
DetailsList, DetailsListLayoutMode, IColumn, IGroupedList, SelectionMode, CheckboxVisibility, IGroup
} from "office-ui-fabric-react/lib/DetailsList";
import {
GroupedList
} from "office-ui-fabric-react/lib/GroupedList";
import {
IViewport
} from "office-ui-fabric-react/lib/utilities/decorators/withViewport";
import { IContextualMenuItem, } from "office-ui-fabric-react/lib/ContextualMenu";
export class PropertyBagGlobalNavState {
public menuitems: Array<IContextualMenuItem>; // The menuItems to display
public errorMessages: Array<md.Message>;// any error messages
}
export default class PropertyBagGlobalNav extends React.Component<IPropertyBagGlobalNavProps, PropertyBagGlobalNavState> {
public constructor(props) {
super(props);
this.state = { menuitems: [], errorMessages: [] };
}
/**
* Extracts the values from a searchResult and adds them to the list of menuItems
* each managedProperty in managedProperties represents a level in the menu. The actial sites
* are added below the last level.
* @param {*} r -- a searchresult
*
* @memberOf PropertyBagGlobalNav
*/
public addMenuItem(r: any): void {
let currentItem: IContextualMenuItem;
let currentSet: Array<IContextualMenuItem> = this.state.menuitems;
debugger;
for (const managedProperty of this.props.managedProperties) {
if (r[managedProperty]) {// if site does not have this property set
const value = r[managedProperty].trim();
currentItem = _.find(currentSet, i => { return i.key === value; });
if (!currentItem) {
const idx = currentSet.push({ key: value, name: value, items: [] });
currentItem = currentSet[idx - 1];
}
currentSet = currentItem.items;
}
}
if (currentItem) { // should have it if site does have this property set
currentItem.items.push({
key: r['Title'],
name: r['Title'],
href: r['SPSiteUrl']
});
}
}
/**
* Gets the list of sites to be displayed in the Menu using the filters specified in
* the PropertyPane
*
* @param {Array<string>} siteTemplatesToInclude Site Templates to be included in the menu
* @param {Array<string>} filters Additional metadata filters to be applid to the list of sites
* @param {Array<string>} managedProperties -- the list of properties used to build the menu
*
* @memberOf PropertyBagGlobalNav
*/
public getSites(siteTemplatesToInclude: Array<string>, filters: Array<string>, managedProperties: Array<string>) {
let querytext = "contentclass:STS_Site ";
if (siteTemplatesToInclude) {
querytext = utils.addSiteTemplatesToSearchQuery(siteTemplatesToInclude, querytext);
}
if (filters) {
querytext = utils.addFiltersToSearchQuery(filters, querytext);
}
const selectProperties: Array<string> = ["Title", "SPSiteUrl"];
for (const managedProperty of managedProperties) {
selectProperties.push(managedProperty);
}
const q: SearchQuery = {
Querytext: querytext,
SelectProperties: selectProperties,
RowLimit: 999,
TrimDuplicates: false,
// SortList:
// [
// {
// Property: 'Title',
// Direction: SortDirection.Ascending
// }
// ]
};
pnp.sp.search(q).then((results: SearchResults) => {
this.state.menuitems = [];
for (const r of results.PrimarySearchResults) {
this.addMenuItem(r);
}
this.setState(this.state);
}).catch(err => {
debugger;
this.state.errorMessages.push(new md.Message(err));
this.setState(this.state);
});
}
/** react lifecycle */
public componentWillMount() {
this.getSites(this.props.siteTemplatesToInclude, this.props.filters, this.props.managedProperties);
}
public componentWillReceiveProps(nextProps: IPropertyBagGlobalNavProps, nextContext: any) {
this.getSites(nextProps.siteTemplatesToInclude, nextProps.filters, nextProps.managedProperties);
}
public render(): React.ReactElement<IPropertyBagGlobalNavProps> {
return (
<CommandBar items={this.state.menuitems} />
);
}
}

View File

@ -0,0 +1,15 @@
define([], function () {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"FiltersFieldLabel": "Metadata Filters",
"FiltersFieldDescription": "A list of filters in the form 'ManagedPropertyName=value' used to filter thhe sites shjown in the list",
"SiteTemplatesToIncludeFieldDescription": "A list of SiteTemplate names and IDs separated by a '#' characher Leave off the SiteTemplate ID to iclude All. (e.g STS#1 will include Teamsites, STS will include All STS Sites)",
"SiteTemplatesToIncludeFieldLabel": "Site Templates",
"ManagedPropertiesFieldDescription": "A list of Managed Properties used to build the menu hierarchy",
"ManagedPropertiesFieldLabel": "Menu Properties"
}
});

View File

@ -0,0 +1,16 @@
declare interface IPropertyBagGlobalNavStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
SiteTemplatesToIncludeFieldLabel: string;
SiteTemplatesToIncludeFieldDescription: string;
FiltersFieldLabel: string;
FiltersFieldDescription: string;
ManagedPropertiesFieldLabel: string;
ManagedPropertiesFieldDescription: string;
}
declare module 'propertyBagGlobalNavStrings' {
const strings: IPropertyBagGlobalNavStrings;
export = strings;
}

View File

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

View File

@ -0,0 +1,9 @@
export default class DisplayProp {
constructor(
public crawledPropertyName: string,
public managedPropertyName?: string,
public value?: string,
public searchable?: boolean,
) { }
}

View File

@ -0,0 +1,58 @@
import * as React from "react";
import { Guid } from "@microsoft/sp-core-library";
import { MessageBar, MessageBarType } from "office-ui-fabric-react/lib/MessageBar";
/**
* A helper class used to hold messages to be displayed in a MessageBar
*
* @export
* @class Message
*/
export class Message {
public Id: string;
public text: string;
public constructor(msg: string) {
this.text = msg;
this.Id = Guid.newGuid().toString();
}
}
export interface IMessageDisplayProps {
messages: Array<Message>;
hideMessage: (messageId) => void;
}
/**
* A class used to Display Messages in the webpart.
*
* @export
* @class MessageDisplay
* @extends {React.Component<IMessageDisplayProps, any>}
*/
export default class MessageDisplay extends React.Component<IMessageDisplayProps, any> {
/**
*
*
*
* @memberOf MessageDisplay
*/
public createDismissHandler = (messageId) => (vale) => {
this.props.hideMessage(messageId);
}
public render(): React.ReactElement<IMessageDisplayProps> {
return (
<div>
{this.props.messages.map((message, y, z) => {
return (
<MessageBar
messageBarType={MessageBarType.remove}
isMultiline={true}
onDismiss={this.createDismissHandler(message.Id)}>
{message.text}
</MessageBar>
);
})}
</div>
);
}
}

View File

@ -0,0 +1,256 @@
import * as _ from "lodash";
import { Web } from "sp-pnp-js";
require("sp-init");
require("microsoft-ajax");
require("sp-runtime");
require("sharepoint");
import DisplayProp from "./DisplayProp";
/**
* Various utilitty functions used by the React-ProprtyBag* Webparts
*
* @export
* @class utils
*/
export default class utils {
//http://vipulkelkar.blogspot.com/2015/09/index-web-property-bag-using-javascript.html
/**
* See ://http://vipulkelkar.blogspot.com/2015/09/index-web-property-bag-using-javascript.html
* Encodes a PropertyKey so that it can be included in the Sharepoint vti_indexedpropertykeys property
*
* @static
* @param {any} propKey The unencoded Property Key
* @returns
*
* @memberOf utils
*/
public static EncodePropertyKey(propKey) {
var bytes = [];
for (var i = 0; i < propKey.length; ++i) {
bytes.push(propKey.charCodeAt(i));
bytes.push(0);
}
var b64encoded = window.btoa(String.fromCharCode.apply(null, bytes));
return b64encoded;
}
/**
* See ://http://vipulkelkar.blogspot.com/2015/09/index-web-property-bag-using-javascript.html
* Decodes a PropertyKey retrieved from the Sharepoint vti_indexedpropertykeys property
*
* @static
* @param {any} propKey The encoded Property Key
* @returns
*
* @memberOf utils
*/
public static DecodePropertyKey(propKey) {
const encoded = window.atob(propKey);
let decoded = "";
for (let x = 0; x < encoded.length; x = x + 2) {
decoded = decoded + encoded.substr(x, 1);
}
return decoded;
}
/**
* Decodes all the searchable Properities recived from Sharepoint
*
* @static
* @param {string} sp The encoded Sarchable Properties String
* @returns {Array<string>} An array of the names of all the searchable properties
*
* @memberOf utils
*/
public static decodeSearchableProps(sp: string): Array<string> {
const searchableprops: Array<string> = [];
if (sp) {
const encodedPropNames = sp.split("|");
for (const encodedPropName of encodedPropNames) {
searchableprops.push(this.DecodePropertyKey(encodedPropName));
}
}
return searchableprops;
}
/**
* AllProperties- the resullsts from web.select("Title", "AllProperties").expand("AllProperties").get()
* propertiesToSelect- The properties you waant to select out of AllProperties
* searchableProperties-- and araay of properties which are known to be searchjable
*
*/
/**
* Extracts a Array of DisplayProp's from AllProperties
*
* @static
* @param {*} AllProperties
* @param {Array<string>} propertiesToSelect The Properties to Select from AllProperties
* @param {Array<string>} searchableProperties An Array of Searchable Properties. These will be marked as seartchable in the results
* @param {boolean} [addMissingProps] Indicates if the method should add a empty property if it is in propertiesToSelect but not in AllProperties
* @returns {Array<DisplayProp>}
*
* @memberOf utils
*/
public static SelectProperties(AllProperties: any, propertiesToSelect: Array<string>, searchableProperties: Array<string>, addMissingProps?: boolean): Array<DisplayProp> {
const DisplayProps: Array<DisplayProp> = [];
for (const propToSelect of propertiesToSelect) {
const displayProp: DisplayProp = new DisplayProp(propToSelect);
displayProp.value = AllProperties[propToSelect];
if (_.find(searchableProperties, sp => { return sp === propToSelect; })) {
displayProp.searchable = true;
}
else {
displayProp.searchable = false;
}
DisplayProps.push(displayProp);
}
return DisplayProps;
}
/**
* Saves a Property into the SharePoint PropertyBag
*
* @static
* @param {string} name The name of the property to set
* @param {string} value The value to set
* @param {string} siteUrl The SPSite to set it in
* @returns
*
* @memberOf utils
*/
public static setSPProperty(name: string, value: string, siteUrl: string) {
return new Promise((resolve, reject) => {
let webProps;
const clientContext = new SP.ClientContext(siteUrl);
const web = clientContext.get_web();
webProps = web.get_allProperties();
webProps.set_item(name, value);
web.update();
webProps = web.get_allProperties();
clientContext.load(web);
clientContext.load(webProps);
clientContext.executeQueryAsync(
(sender, args) => { resolve(); },
(sender, args) => { reject(args.get_message()); }
);
});
}
/**
* Sets the values of the propnames parameter to be searchable in the selected site
*
* @static
* @param {string} siteUrl
* @param {Array<string>} propnames
* @returns {Promise<any>}
*
* @memberOf utils
*/
public static saveSearchablePropertiesToSharePoint(siteUrl: string, propnames: Array<string>): Promise<any> {
const encodedPropNames: Array<string> = [];
for (const propname of propnames) {
if (propname != "") {
encodedPropNames.push(this.EncodePropertyKey(propname));
}
}
return this.setSPProperty("vti_indexedpropertykeys", encodedPropNames.join("|") + "|", siteUrl);//need the pipe at the end too?
}
/**
* Forces a full crawl of a site by incrementing the vti_searchversion property
*
* @static
* @param {string} siteUrl The site to force a full crawl on
* @returns {Promise<any>}
*
* @memberOf utils
*/
public static forceCrawl(siteUrl: string): Promise<any> {
const web = new Web(siteUrl);
return web.select("Title", "AllProperties").expand("AllProperties").get().then(r => {
let version: number = r.AllProperties["vti_x005f_searchversion"];
if (version) {
version++;
this.setSPProperty("AllProperties", version.toString(), siteUrl);
}
});
}
/**
* Adds the siteTemplates as filter parameters to queryText
*
* @static
* @param {Array<string>} siteTemplates The Site templates to be included
* @param {string} querytext The queryText to add the filter to
* @returns {string} The new queryText with the filter included
*
* @memberOf utils
*/
public static addSiteTemplatesToSearchQuery(siteTemplates: Array<string>, querytext: string): string {
let newQueryText = querytext.valueOf();
if (siteTemplates.length > 0 && siteTemplates[0] !== "") {
newQueryText += " AND (";
for (const siteTemplate of siteTemplates) {
const siteTemplateParts = siteTemplate.split("#");
if (!siteTemplateParts[1]) {
newQueryText += "SiteTemplate=" + siteTemplateParts[0];
}
else {
newQueryText += "(SiteTemplate=" + siteTemplateParts[0] + " AND SiteTemplateId=" + siteTemplateParts[1] + ")";
}
if (siteTemplates.indexOf(siteTemplate) !== siteTemplates.length - 1) {
newQueryText += " OR ";
}
}
newQueryText += " )";
}
return newQueryText;
}
/**
* Adds filters to the querytext. Filters are OR'd together
*
* @static
* @param {Array<string>} filters The filters to add (in the form ManagedPropertyName=Value)
* @param {string} querytext
* @returns {string}
*
* @memberOf utils
*/
public static addFiltersToSearchQuery(filters: Array<string>, querytext: string): string {
let newQueryText = querytext.valueOf();
if (filters.length > 0 && filters[0] !== "") {
newQueryText += " AND ( ";
for (const filter of filters) {
newQueryText += " " + filter + " ";
if (filters.indexOf(filter) !== filters.length - 1) {
newQueryText += " OR ";
}
}
newQueryText += " )";
}
return newQueryText;
}
/**
* Parses a string that is returned from an SPFX Multiline Input Paramater inito an array of strings,
* removinng blank entries
*
*
* @static
* @param {string} value The value from the SPFX Multiline Input Paramater
* @returns {Array<string>}
*
* @memberOf utils
*/
public static parseMultilineTextToArray(value: string): Array<string> {
if (!value) {
return [];
}
return value.split('\n').filter(val => { return val.trim() != ""; });
}
}

View File

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

View File

@ -0,0 +1,6 @@
{
"globalDependencies": {
"microsoft.ajax": "registry:dt/microsoft.ajax#0.0.0+20160706234802",
"sharepoint": "registry:dt/sharepoint#2010.1.0+20170120191654"
}
}

View File

@ -0,0 +1,5 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds */
declare const UNIT_TEST: boolean;

View File

@ -0,0 +1,15 @@
// Type definitions for assertion-error 1.0.0
// Project: https://github.com/chaijs/assertion-error
// Definitions by: Bart van der Schoor <https://github.com/Bartvds>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
declare module 'assertion-error' {
class AssertionError implements Error {
constructor(message: string, props?: any, ssf?: Function);
name: string;
message: string;
showDiff: boolean;
stack: string;
}
export = AssertionError;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
{
"resolution": "main",
"tree": {
"src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/436f368f9a224967984882dd5642bbe001da6268/microsoft-ajax/microsoft.ajax.d.ts",
"raw": "registry:dt/microsoft.ajax#0.0.0+20160706234802",
"typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/436f368f9a224967984882dd5642bbe001da6268/microsoft-ajax/microsoft.ajax.d.ts"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
{
"resolution": "main",
"tree": {
"src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/0705aae93a46216181168fe463fa793a07a9b061/sharepoint/index.d.ts",
"raw": "registry:dt/sharepoint#2010.1.0+20170120191654",
"typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/0705aae93a46216181168fe463fa793a07a9b061/sharepoint/index.d.ts"
}
}

View File

@ -0,0 +1,2 @@
/// <reference path="globals/microsoft.ajax/index.d.ts" />
/// <reference path="globals/sharepoint/index.d.ts" />

View File

@ -0,0 +1,631 @@
// Type definitions for Knockout v3.2.0
// Project: http://knockoutjs.com
// Definitions by: Boris Yankov <https://github.com/borisyankov/>, Igor Oleinikov <https://github.com/Igorbek/>, Clément Bourgeois <https://github.com/moonpyk/>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
interface KnockoutSubscribableFunctions<T> {
[key: string]: KnockoutBindingHandler;
notifySubscribers(valueToWrite?: T, event?: string): void;
}
interface KnockoutComputedFunctions<T> {
[key: string]: KnockoutBindingHandler;
}
interface KnockoutObservableFunctions<T> {
[key: string]: KnockoutBindingHandler;
equalityComparer(a: any, b: any): boolean;
}
interface KnockoutObservableArrayFunctions<T> {
// General Array functions
indexOf(searchElement: T, fromIndex?: number): number;
slice(start: number, end?: number): T[];
splice(start: number): T[];
splice(start: number, deleteCount: number, ...items: T[]): T[];
pop(): T;
push(...items: T[]): void;
shift(): T;
unshift(...items: T[]): number;
reverse(): KnockoutObservableArray<T>;
sort(): KnockoutObservableArray<T>;
sort(compareFunction: (left: T, right: T) => number): KnockoutObservableArray<T>;
// Ko specific
[key: string]: KnockoutBindingHandler;
replace(oldItem: T, newItem: T): void;
remove(item: T): T[];
remove(removeFunction: (item: T) => boolean): T[];
removeAll(items: T[]): T[];
removeAll(): T[];
destroy(item: T): void;
destroy(destroyFunction: (item: T) => boolean): void;
destroyAll(items: T[]): void;
destroyAll(): void;
}
interface KnockoutSubscribableStatic {
fn: KnockoutSubscribableFunctions<any>;
new <T>(): KnockoutSubscribable<T>;
}
interface KnockoutSubscription {
dispose(): void;
}
interface KnockoutSubscribable<T> extends KnockoutSubscribableFunctions<T> {
subscribe(callback: (newValue: T) => void, target?: any, event?: string): KnockoutSubscription;
subscribe<TEvent>(callback: (newValue: TEvent) => void, target: any, event: string): KnockoutSubscription;
extend(requestedExtenders: { [key: string]: any; }): KnockoutSubscribable<T>;
getSubscriptionsCount(): number;
}
interface KnockoutComputedStatic {
fn: KnockoutComputedFunctions<any>;
<T>(): KnockoutComputed<T>;
<T>(func: () => T, context?: any, options?: any): KnockoutComputed<T>;
<T>(def: KnockoutComputedDefine<T>, context?: any): KnockoutComputed<T>;
}
interface KnockoutComputed<T> extends KnockoutObservable<T>, KnockoutComputedFunctions<T> {
fn: KnockoutComputedFunctions<any>;
dispose(): void;
isActive(): boolean;
getDependenciesCount(): number;
extend(requestedExtenders: { [key: string]: any; }): KnockoutComputed<T>;
}
interface KnockoutObservableArrayStatic {
fn: KnockoutObservableArrayFunctions<any>;
<T>(value?: T[]): KnockoutObservableArray<T>;
}
interface KnockoutObservableArray<T> extends KnockoutObservable<T[]>, KnockoutObservableArrayFunctions<T> {
extend(requestedExtenders: { [key: string]: any; }): KnockoutObservableArray<T>;
}
interface KnockoutObservableStatic {
fn: KnockoutObservableFunctions<any>;
<T>(value?: T): KnockoutObservable<T>;
}
interface KnockoutObservable<T> extends KnockoutSubscribable<T>, KnockoutObservableFunctions<T> {
(): T;
(value: T): void;
peek(): T;
valueHasMutated?:{(): void;};
valueWillMutate?:{(): void;};
extend(requestedExtenders: { [key: string]: any; }): KnockoutObservable<T>;
}
interface KnockoutComputedDefine<T> {
read(): T;
write? (value: T): void;
disposeWhenNodeIsRemoved?: Node;
disposeWhen? (): boolean;
owner?: any;
deferEvaluation?: boolean;
pure?: boolean;
}
interface KnockoutBindingContext {
$parent: any;
$parents: any[];
$root: any;
$data: any;
$rawData: any | KnockoutObservable<any>;
$index?: KnockoutObservable<number>;
$parentContext?: KnockoutBindingContext;
$component: any;
$componentTemplateNodes: Node[];
extend(properties: any): any;
createChildContext(dataItemOrAccessor: any, dataItemAlias?: any, extendCallback?: Function): any;
}
interface KnockoutAllBindingsAccessor {
(): any;
get(name: string): any;
has(name: string): boolean;
}
interface KnockoutBindingHandler {
after?: Array<string>;
init?: (element: any, valueAccessor: () => any, allBindingsAccessor?: KnockoutAllBindingsAccessor, viewModel?: any, bindingContext?: KnockoutBindingContext) => void | { controlsDescendantBindings: boolean; };
update?: (element: any, valueAccessor: () => any, allBindingsAccessor?: KnockoutAllBindingsAccessor, viewModel?: any, bindingContext?: KnockoutBindingContext) => void;
options?: any;
preprocess?: (value: string, name: string, addBindingCallback?: (name: string, value: string) => void) => string;
}
interface KnockoutBindingHandlers {
[bindingHandler: string]: KnockoutBindingHandler;
// Controlling text and appearance
visible: KnockoutBindingHandler;
text: KnockoutBindingHandler;
html: KnockoutBindingHandler;
css: KnockoutBindingHandler;
style: KnockoutBindingHandler;
attr: KnockoutBindingHandler;
// Control Flow
foreach: KnockoutBindingHandler;
if: KnockoutBindingHandler;
ifnot: KnockoutBindingHandler;
with: KnockoutBindingHandler;
// Working with form fields
click: KnockoutBindingHandler;
event: KnockoutBindingHandler;
submit: KnockoutBindingHandler;
enable: KnockoutBindingHandler;
disable: KnockoutBindingHandler;
value: KnockoutBindingHandler;
textInput: KnockoutBindingHandler;
hasfocus: KnockoutBindingHandler;
checked: KnockoutBindingHandler;
options: KnockoutBindingHandler;
selectedOptions: KnockoutBindingHandler;
uniqueName: KnockoutBindingHandler;
// Rendering templates
template: KnockoutBindingHandler;
// Components (new for v3.2)
component: KnockoutBindingHandler;
}
interface KnockoutMemoization {
memoize(callback: () => string): string;
unmemoize(memoId: string, callbackParams: any[]): boolean;
unmemoizeDomNodeAndDescendants(domNode: any, extraCallbackParamsArray: any[]): boolean;
parseMemoText(memoText: string): string;
}
interface KnockoutVirtualElement {}
interface KnockoutVirtualElements {
allowedBindings: { [bindingName: string]: boolean; };
emptyNode(node: KnockoutVirtualElement ): void;
firstChild(node: KnockoutVirtualElement ): KnockoutVirtualElement;
insertAfter( container: KnockoutVirtualElement, nodeToInsert: Node, insertAfter: Node ): void;
nextSibling(node: KnockoutVirtualElement): Node;
prepend(node: KnockoutVirtualElement, toInsert: Node ): void;
setDomNodeChildren(node: KnockoutVirtualElement, newChildren: { length: number;[index: number]: Node; } ): void;
childNodes(node: KnockoutVirtualElement ): Node[];
}
interface KnockoutExtenders {
throttle(target: any, timeout: number): KnockoutComputed<any>;
notify(target: any, notifyWhen: string): any;
rateLimit(target: any, timeout: number): any;
rateLimit(target: any, options: { timeout: number; method?: string; }): any;
trackArrayChanges(target: any): any;
}
//
// NOTE TO MAINTAINERS AND CONTRIBUTORS : pay attention to only include symbols that are
// publicly exported in the minified version of ko, without that you can give the false
// impression that some functions will be available in production builds.
//
interface KnockoutUtils {
//////////////////////////////////
// utils.domData.js
//////////////////////////////////
domData: {
get (node: Element, key: string): any;
set (node: Element, key: string, value: any): void;
getAll(node: Element, createIfNotFound: boolean): any;
clear(node: Element): boolean;
};
//////////////////////////////////
// utils.domNodeDisposal.js
//////////////////////////////////
domNodeDisposal: {
addDisposeCallback(node: Element, callback: Function): void;
removeDisposeCallback(node: Element, callback: Function): void;
cleanNode(node: Node): Element;
removeNode(node: Node): void;
};
addOrRemoveItem<T>(array: T[] | KnockoutObservable<T>, value: T, included: T): void;
arrayFilter<T>(array: T[], predicate: (item: T) => boolean): T[];
arrayFirst<T>(array: T[], predicate: (item: T) => boolean, predicateOwner?: any): T;
arrayForEach<T>(array: T[], action: (item: T, index: number) => void): void;
arrayGetDistinctValues<T>(array: T[]): T[];
arrayIndexOf<T>(array: T[], item: T): number;
arrayMap<T, U>(array: T[], mapping: (item: T) => U): U[];
arrayPushAll<T>(array: T[] | KnockoutObservableArray<T>, valuesToPush: T[]): T[];
arrayRemoveItem(array: any[], itemToRemove: any): void;
compareArrays<T>(a: T[], b: T[]): Array<KnockoutArrayChange<T>>;
extend(target: Object, source: Object): Object;
fieldsIncludedWithJsonPost: any[];
getFormFields(form: any, fieldName: string): any[];
objectForEach(obj: any, action: (key: any, value: any) => void): void;
parseHtmlFragment(html: string): any[];
parseJson(jsonString: string): any;
postJson(urlOrForm: any, data: any, options: any): void;
peekObservable<T>(value: KnockoutObservable<T>): T;
range(min: any, max: any): any;
registerEventHandler(element: any, eventType: any, handler: Function): void;
setHtml(node: Element, html: () => string): void;
setHtml(node: Element, html: string): void;
setTextContent(element: any, textContent: string | KnockoutObservable<string>): void;
stringifyJson(data: any, replacer?: Function, space?: string): string;
toggleDomNodeCssClass(node: any, className: string, shouldHaveClass: boolean): void;
triggerEvent(element: any, eventType: any): void;
unwrapObservable<T>(value: KnockoutObservable<T> | T): T;
// NOT PART OF THE MINIFIED API SURFACE (ONLY IN knockout-{version}.debug.js) https://github.com/SteveSanderson/knockout/issues/670
// forceRefresh(node: any): void;
// ieVersion: number;
// isIe6: boolean;
// isIe7: boolean;
// jQueryHtmlParse(html: string): any[];
// makeArray(arrayLikeObject: any): any[];
// moveCleanedNodesToContainerElement(nodes: any[]): HTMLElement;
// replaceDomNodes(nodeToReplaceOrNodeArray: any, newNodesArray: any[]): void;
// setDomNodeChildren(domNode: any, childNodes: any[]): void;
// setElementName(element: any, name: string): void;
// setOptionNodeSelectionState(optionNode: any, isSelected: boolean): void;
// simpleHtmlParse(html: string): any[];
// stringStartsWith(str: string, startsWith: string): boolean;
// stringTokenize(str: string, delimiter: string): string[];
// stringTrim(str: string): string;
// tagNameLower(element: any): string;
}
interface KnockoutArrayChange<T> {
status: string;
value: T;
index: number;
moved?: number;
}
//////////////////////////////////
// templateSources.js
//////////////////////////////////
interface KnockoutTemplateSourcesDomElement {
text(): any;
text(value: any): void;
data(key: string): any;
data(key: string, value: any): any;
}
interface KnockoutTemplateAnonymous extends KnockoutTemplateSourcesDomElement {
nodes(): any;
nodes(value: any): void;
}
interface KnockoutTemplateSources {
domElement: {
prototype: KnockoutTemplateSourcesDomElement
new (element: Element): KnockoutTemplateSourcesDomElement
};
anonymousTemplate: {
prototype: KnockoutTemplateAnonymous;
new (element: Element): KnockoutTemplateAnonymous;
};
}
//////////////////////////////////
// nativeTemplateEngine.js
//////////////////////////////////
interface KnockoutNativeTemplateEngine {
renderTemplateSource(templateSource: Object, bindingContext?: KnockoutBindingContext, options?: Object): any[];
}
//////////////////////////////////
// templateEngine.js
//////////////////////////////////
interface KnockoutTemplateEngine extends KnockoutNativeTemplateEngine {
createJavaScriptEvaluatorBlock(script: string): string;
makeTemplateSource(template: any, templateDocument?: Document): any;
renderTemplate(template: any, bindingContext: KnockoutBindingContext, options: Object, templateDocument: Document): any;
isTemplateRewritten(template: any, templateDocument: Document): boolean;
rewriteTemplate(template: any, rewriterCallback: Function, templateDocument: Document): void;
}
/////////////////////////////////
interface KnockoutStatic {
utils: KnockoutUtils;
memoization: KnockoutMemoization;
bindingHandlers: KnockoutBindingHandlers;
getBindingHandler(handler: string): KnockoutBindingHandler;
virtualElements: KnockoutVirtualElements;
extenders: KnockoutExtenders;
applyBindings(viewModelOrBindingContext?: any, rootNode?: any): void;
applyBindingsToDescendants(viewModelOrBindingContext: any, rootNode: any): void;
applyBindingAccessorsToNode(node: Node, bindings: (bindingContext: KnockoutBindingContext, node: Node) => {}, bindingContext: KnockoutBindingContext): void;
applyBindingAccessorsToNode(node: Node, bindings: {}, bindingContext: KnockoutBindingContext): void;
applyBindingAccessorsToNode(node: Node, bindings: (bindingContext: KnockoutBindingContext, node: Node) => {}, viewModel: any): void;
applyBindingAccessorsToNode(node: Node, bindings: {}, viewModel: any): void;
applyBindingsToNode(node: Node, bindings: any, viewModelOrBindingContext?: any): any;
subscribable: KnockoutSubscribableStatic;
observable: KnockoutObservableStatic;
computed: KnockoutComputedStatic;
pureComputed<T>(evaluatorFunction: () => T, context?: any): KnockoutComputed<T>;
pureComputed<T>(options: KnockoutComputedDefine<T>, context?: any): KnockoutComputed<T>;
observableArray: KnockoutObservableArrayStatic;
contextFor(node: any): any;
isSubscribable(instance: any): boolean;
toJSON(viewModel: any, replacer?: Function, space?: any): string;
toJS(viewModel: any): any;
isObservable(instance: any): boolean;
isWriteableObservable(instance: any): boolean;
isComputed(instance: any): boolean;
dataFor(node: any): any;
removeNode(node: Element): void;
cleanNode(node: Element): Element;
renderTemplate(template: Function, viewModel: any, options?: any, target?: any, renderMode?: any): any;
renderTemplate(template: string, viewModel: any, options?: any, target?: any, renderMode?: any): any;
unwrap<T>(value: KnockoutObservable<T> | T): T;
computedContext: KnockoutComputedContext;
//////////////////////////////////
// templateSources.js
//////////////////////////////////
templateSources: KnockoutTemplateSources;
//////////////////////////////////
// templateEngine.js
//////////////////////////////////
templateEngine: {
prototype: KnockoutTemplateEngine;
new (): KnockoutTemplateEngine;
};
//////////////////////////////////
// templateRewriting.js
//////////////////////////////////
templateRewriting: {
ensureTemplateIsRewritten(template: Node, templateEngine: KnockoutTemplateEngine, templateDocument: Document): any;
ensureTemplateIsRewritten(template: string, templateEngine: KnockoutTemplateEngine, templateDocument: Document): any;
memoizeBindingAttributeSyntax(htmlString: string, templateEngine: KnockoutTemplateEngine): any;
applyMemoizedBindingsToNextSibling(bindings: any, nodeName: string): string;
};
//////////////////////////////////
// nativeTemplateEngine.js
//////////////////////////////////
nativeTemplateEngine: {
prototype: KnockoutNativeTemplateEngine;
new (): KnockoutNativeTemplateEngine;
instance: KnockoutNativeTemplateEngine;
};
//////////////////////////////////
// jqueryTmplTemplateEngine.js
//////////////////////////////////
jqueryTmplTemplateEngine: {
prototype: KnockoutTemplateEngine;
renderTemplateSource(templateSource: Object, bindingContext: KnockoutBindingContext, options: Object): Node[];
createJavaScriptEvaluatorBlock(script: string): string;
addTemplate(templateName: string, templateMarkup: string): void;
};
//////////////////////////////////
// templating.js
//////////////////////////////////
setTemplateEngine(templateEngine: KnockoutNativeTemplateEngine): void;
renderTemplate(template: Function, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any;
renderTemplate(template: any, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any;
renderTemplate(template: Function, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any;
renderTemplate(template: any, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any;
renderTemplate(template: Function, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any;
renderTemplate(template: any, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any;
renderTemplate(template: Function, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any;
renderTemplate(template: any, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any;
renderTemplateForEach(template: Function, arrayOrObservableArray: any[], options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any;
renderTemplateForEach(template: any, arrayOrObservableArray: any[], options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any;
renderTemplateForEach(template: Function, arrayOrObservableArray: KnockoutObservable<any>, options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any;
renderTemplateForEach(template: any, arrayOrObservableArray: KnockoutObservable<any>, options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any;
expressionRewriting: {
bindingRewriteValidators: any;
parseObjectLiteral: { (objectLiteralString: string): any[] }
};
/////////////////////////////////
bindingProvider: {
instance: KnockoutBindingProvider;
new (): KnockoutBindingProvider;
}
/////////////////////////////////
// selectExtensions.js
/////////////////////////////////
selectExtensions: {
readValue(element: HTMLElement): any;
writeValue(element: HTMLElement, value: any): void;
};
components: KnockoutComponents;
}
interface KnockoutBindingProvider {
nodeHasBindings(node: Node): boolean;
getBindings(node: Node, bindingContext: KnockoutBindingContext): {};
getBindingAccessors?(node: Node, bindingContext: KnockoutBindingContext): { [key: string]: string; };
}
interface KnockoutComputedContext {
getDependenciesCount(): number;
isInitial: () => boolean;
isSleeping: boolean;
}
//
// refactored types into a namespace to reduce global pollution
// and used Union Types to simplify overloads (requires TypeScript 1.4)
//
declare module KnockoutComponentTypes {
interface Config {
viewModel?: ViewModelFunction | ViewModelSharedInstance | ViewModelFactoryFunction | AMDModule;
template: string | Node[]| DocumentFragment | TemplateElement | AMDModule;
synchronous?: boolean;
}
interface ComponentConfig {
viewModel?: ViewModelFunction | ViewModelSharedInstance | ViewModelFactoryFunction | AMDModule;
template: any;
createViewModel?: any;
}
interface EmptyConfig {
}
// common AMD type
interface AMDModule {
require: string;
}
// viewmodel types
interface ViewModelFunction {
(params?: any): any;
}
interface ViewModelSharedInstance {
instance: any;
}
interface ViewModelFactoryFunction {
createViewModel: (params?: any, componentInfo?: ComponentInfo) => any;
}
interface ComponentInfo {
element: Node;
templateNodes: Node[];
}
interface TemplateElement {
element: string | Node;
}
interface Loader {
getConfig? (componentName: string, callback: (result: ComponentConfig) => void): void;
loadComponent? (componentName: string, config: ComponentConfig, callback: (result: Definition) => void): void;
loadTemplate? (componentName: string, templateConfig: any, callback: (result: Node[]) => void): void;
loadViewModel? (componentName: string, viewModelConfig: any, callback: (result: any) => void): void;
suppressLoaderExceptions?: boolean;
}
interface Definition {
template: Node[];
createViewModel? (params: any, options: { element: Node; }): any;
}
}
interface KnockoutComponents {
// overloads for register method:
register(componentName: string, config: KnockoutComponentTypes.Config | KnockoutComponentTypes.EmptyConfig): void;
isRegistered(componentName: string): boolean;
unregister(componentName: string): void;
get(componentName: string, callback: (definition: KnockoutComponentTypes.Definition) => void): void;
clearCachedDefinition(componentName: string): void
defaultLoader: KnockoutComponentTypes.Loader;
loaders: KnockoutComponentTypes.Loader[];
getComponentNameForNode(node: Node): string;
}
declare var ko: KnockoutStatic;
declare module "knockout" {
export = ko;
}

View File

@ -0,0 +1,4 @@
/// <reference path="@ms/odsp.d.ts" />
/// <reference path="assertion-error/assertion-error.d.ts" />
/// <reference path="knockout/knockout.d.ts" />
/// <referehce path="SP/SP.d.ts />