New sample - React sitepages metadata (#481)

* Added react-sitepages-metadata sample

* updated readme

*  Added react-sitepages-metadata sample

* Fix for dialog rendering tag pickers with no initial value
This commit is contained in:
Oleg Rumiancev 2018-04-27 11:18:36 -04:00 committed by Vesa Juvonen
parent 6899e6d7fe
commit 6a59355c8b
33 changed files with 19996 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "react-sitepages-metadata",
"libraryId": "46c3c432-396d-4e9b-944d-6bf8c5466d0c",
"environment": "spo"
}
}

View File

@ -0,0 +1,69 @@
# react-sitepages-metadata
## Summary
Solution provides an enhancement to SitePages library that enables updating existing items with metadata, and a rollup WebPart to display them.
### News rollup WebPart
![News rollup WebPart](./assets/demo-wp.gif)
### SitePages library CommandSet
![SitePages library CommandSet](./assets/demo-commandset.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
> If you plan on using included PowerShell script make sure you have [PnP PowerShell](https://github.com/SharePoint/PnP-PowerShell) installed
## Solution
Solution|Author(s)
--------|---------
react-sitepages-metadata | Oleg Rumiancev ([LinkedIn](https://linkedin.com/in/olegrumiancev), [Twitter (olezhka_lt)](https://twitter.com/olezhka_lt))
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 17, 2018|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Features
Solution provides an enhancement to SitePages library that enables updating existing items with metadata (implemented as Lookup fields).
Relies heavily on Office UI Fabric.
Contains following elements:
- Metadata-News rollup WebPart - displays published SitePages items (promoted to news), provides filtering and paging capabilities, as well as many configuration options
- CommandSet extension for SitePages library - a button appears when a single item is selected. Clicking on the item shows a fillable dialog with lookup fields
### Important notes
- Changes in some WebPart properties will not be reflected after hitting "Apply" button - please refresh the page, too
- To see a collection of lookup fields in the WebPart property pane change/edit any of the instantly visible properties, such as Item limit, width or item height. Reason for this is SitePages library will be queried for lookup lists only after the property pane is initially opened
### Resources
- [React Quick Start](https://facebook.github.io/react/docs/tutorial.html)
- [TypeScript React Tutorials](https://www.typescriptlang.org/docs/handbook/react-&-webpack.html)
- [Office UI Fabric](https://dev.office.com/fabric)
## Path to Awesome
- Clone this repository
- [Optional] run cofigure-lists.ps1 in install folder to create lookup lists and fields (amend according to your needs).
- [Optional] fill lookup lists with items and publish some items in SitePages library
- in the command line run:
- `git clone the rero`
- `npm install`
- `gulp serve --nobrowser`
- navigate to `https://<tenant>.sharepoint.com/sites/<target site>/_layouts/workbench.aspx`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-sitepages-metadata" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -0,0 +1,28 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"metadata-news-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/metadataNews/MetadataNewsWebPart.js",
"manifest": "./src/webparts/metadataNews/MetadataNewsWebPart.manifest.json"
}
]
},
"metadata-sitepages-command-set": {
"components": [
{
"entrypoint": "./lib/extensions/pagesMetadata/MetadataSitePagesCommandSet.js",
"manifest": "./src/extensions/pagesMetadata/MetadataSitePagesCommandSet.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"MetadataNewsWebPartStrings": "lib/webparts/metadataNews/loc/{locale}.js",
"MetadataSitePagesCommandSetStrings": "lib/extensions/pagesMetadata/loc/{locale}.js",
"ControlStrings": "./node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-sitepages-metadata-client-side-solution",
"id": "46c3c432-396d-4e9b-944d-6bf8c5466d0c",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"features": [
{
"title": "Command set for SitePages",
"description": "deplys command set",
"id": "e91d5532-3519-4b50-b55e-b142fc74cd8c",
"version": "1.0.0.0",
"assets": {
"elementManifests": [
"elements.xml"
]
}
}
]
},
"paths": {
"zippedPackage": "solution/react-sitepages-metadata.sppkg"
}
}

View File

@ -0,0 +1,36 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
},
"serveConfigurations": {
"default": {
"pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
"customActions": {
"d4fd389a-c02f-41ac-977a-474020daa4c6": {
"location": "ClientSideExtension.ListViewCommandSet.CommandBar",
"properties": {
"sampleTextOne": "One item is selected in the list",
"sampleTextTwo": "This command is always visible."
}
}
}
},
"metadataSitePages": {
"pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
"customActions": {
"d4fd389a-c02f-41ac-977a-474020daa4c6": {
"location": "ClientSideExtension.ListViewCommandSet.CommandBar",
"properties": {
"sampleTextOne": "One item is selected in the list",
"sampleTextTwo": "This command is always visible."
}
}
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,44 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);
gulp.task('version-sync', function () {
// import gulp utilits to write error messages
const gutil = require('gulp-util');
// import file system utilities form nodeJS
const fs = require('fs');
// read package.json
var pkgConfig = require('./package.json');
// read configuration of web part solution file
var pkgSolution = require('./config/package-solution.json');
// log old version
gutil.log('Old Version:\t' + pkgSolution.solution.version);
// Generate new MS compliant version number
var newVersionNumber = pkgConfig.version.split('-')[0] + '.0';
// assign newly generated version number to web part version
pkgSolution.solution.version = newVersionNumber;
// log new version
gutil.log('New Version:\t' + pkgSolution.solution.version);
// write changed package-solution file
fs.writeFile('./config/package-solution.json', JSON.stringify(pkgSolution, null, 4));
});
var runSequence = require('run-sequence');
gulp.task('package', function (cb) {
runSequence(['bundle', 'package-solution'], cb);
});

View File

@ -0,0 +1,51 @@
function EnsureLookupField($fieldName, $fieldDisplayName, $targetList, $lookupList) {
$field = $null
$field = Get-PnPField -List $targetList -Identity $fieldName -ErrorAction SilentlyContinue
if ($field -eq $null) {
$guid = [System.Guid]::NewGuid().ToString("B")
$xml = "<Field Type='Lookup' `
DisplayName='$fieldDisplayName' `
Required='FALSE' `
EnforceUniqueValues='FALSE' UnlimitedLengthInDocumentLibrary='FALSE' RelationshipDeleteBehavior='None' `
List='$($lookupList.Id.ToString("B"))' `
ShowField='Title' `
ID='$guid' `
Name='$fieldName' />"
$field = Add-PnPFieldFromXml -List $targetList -FieldXml $xml
$targetList.Update()
$dv = $targetList.DefaultView
$dv.ViewFields.Add($fieldName)
$dv.Update()
}
}
function EnsureLookupStructure($lookupsList) {
$lstSitePages = Get-PnpList -Identity "SitePages"
$lookupsList | Foreach {
# ensure list
$list = $null
$list = Get-PnPList -Identity $_.InternalName
if ($list -eq $null) {
$list = New-PnPList -Template GenericList -Url $_.InternalName -Title $_.Title
$list = Get-PnPList -Identity $_.InternalName
# ensure lookup field in SitePAges
EnsureLookupField $_.InternalName $_.Title $lstSitePages $list
}
}
}
############################
# ENTRY POINT
############################
$targetUrl = "https://<your-tenant>.sharepoint.com/sites/<target-site>"
Connect-PnPOnline -Url $targetUrl -UseWebLogin
# Fill the list with internal name / title pairs for lookup lists / fields
$lookupsList = @()
$lookupsList += @{InternalName="Category";Title="Category"}
$lookupsList += @{InternalName="CustomValue";Title="Custom value"}
EnsureLookupStructure $lookupsList

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
{
"name": "react-sitepages-metadata",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"package": "gulp bundle --ship & gulp package-solution --ship",
"package-dev": "gulp bundle & gulp package-solution",
"postversion": "gulp version-sync"
},
"dependencies": {
"@microsoft/decorators": "~1.4.1",
"@microsoft/office-ui-fabric-react-bundle": "^1.4.1",
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-dialog": "~1.4.1",
"@microsoft/sp-listview-extensibility": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@pnp/graph": "^1.0.4",
"@pnp/spfx-controls-react": "1.2.3",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"array.from": "^1.0.3",
"jquery": "^3.3.1",
"jqueryui": "^1.11.1",
"react": "15.6.2",
"react-dom": "15.6.2",
"react-stonecutter": "^0.3.9",
"react-truncate": "^2.3.0",
"sp-client-custom-fields": "^1.3.7",
"sp-pnp-js": "^3.0.5"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"run-sequence": "^2.2.1"
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Title="metadataSitePages"
RegistrationId="119"
RegistrationType="List"
Location="ClientSideExtension.ListViewCommandSet.CommandBar"
ClientSideComponentId="d4fd389a-c02f-41ac-977a-474020daa4c6"
ClientSideComponentProperties="{}">
</CustomAction>
</Elements>

View File

@ -0,0 +1,191 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
@mixin accentColoring {
background-color: $ms-color-themeDark !important;
color: white !important;
}
@mixin accentColoringDarker {
background-color: $ms-color-themeDarker !important;
color: white !important;
}
@media all and (max-width: 600px) {
.dialogMainOverride {
min-width: 95%;
}
}
@media all and (min-width: 601px) {
.dialogMainOverride {
min-width: 85%;
}
}
.tagPicker {
div[class*="ms-TagItem "] {
background-color: #130707;
}
}
.dialogMainOverride {
background-color: white;
}
.errorCallout {
padding: 10px;
max-width: 450px;
.header {
font-size: 20px;
}
.content {
border-top: 1px solid $ms-color-themeDarker;
}
}
#newsContainer {
position: inherit;
}
.docCardActivity {
div[class*="docCardContents"] {
text-overflow: "ellipsis";
overflow: "hidden";
margin-left: 16px;
margin-right: 16px;
}
div[class*="ms-DocumentCardActivity-avatars"] {
display: none;
}
div[class*="ms-DocumentCardActivity-details"] {
left: 0;
position: relative;
}
}
.menuItem {
@include accentColoring();
> button {
@include accentColoring();
&:hover {
@include accentColoringDarker();
}
i[class*="itemChevronDown"], i[class*="itemChevronUp"] {
color: white !important;
}
}
> button.is-expanded {
@include accentColoringDarker();
}
&:hover, .is-expanded {
@include accentColoringDarker();
}
}
.metadataNews {
.commandBar {
@include accentColoring();
}
.container {
max-width: 900px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
padding: 10px;
}
.docCards {
> div {
background-color: white !important;
color: black !important;
margin: 0 auto;
height: inherit;
max-width: 980px;
> div > div {
// height: inherit;
max-height: inherit;
max-width: inherit;
}
> div + div {
height: inherit;
max-height: inherit;
margin: 0 auto;
div[class*="ms-DocumentCardTitle"], span[class*="ms-DocumentCardActivity-name"] {
color: #1f1f1f;
}
span[class*="ms-DocumentCardActivity-activity"] {
color: #908989;
}
div[class*="ms-DocumentCardActivity activity_"] {
padding-top: 3px;
padding-bottom: 7px;
}
}
}
}
.column {
@include ms-Grid-col;
@include ms-sm12;
@include ms-lg12;
@include ms-xl12;
// @include ms-xlPush1;
// @include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,356 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';
import {
autobind, PrimaryButton, Button,
DialogFooter, DialogContent,
Spinner, SpinnerSize,
TagPicker, ITag, Label, Callout
} from '@microsoft/office-ui-fabric-react-bundle'; // 'office-ui-fabric-react';
import * as pnp from '@pnp/sp';
import {
IFillMetadataDialogContentProps, IFillMetadataDialogContentState, IMetadataNewsItem,
IMetadataRefinerInfo, ILookupInfo, ILookupFieldValue, unescapeHTML
} from '../../interfaces';
import styles from './FillMetadataDialog.module.scss';
class FillMetadataDialogContent extends React.Component<IFillMetadataDialogContentProps, IFillMetadataDialogContentState> {
private saveButtonElement: HTMLElement | null;
constructor(props) {
super(props);
this.state = {
loading: true,
submitting: false,
error: null,
hasError: false,
loadedItem: null
};
pnp.sp.setup({
sp: {
headers: {
Accept: 'application/json;odata=verbose'
},
baseUrl: this.props.webUrl
}
});
}
// To make component render quickly the actual data retrieval starts after initial render in componentDidMount event
public componentDidMount() {
this.loadItem();
}
// Rendering logic
public render(): JSX.Element {
return (
this.state.loading ?
(
<Spinner size={SpinnerSize.large} label='Loading...' ariaLive='assertive' />
) :
(<DialogContent
title={this.state.loadedItem.title}
onDismiss={this.props.close}
showCloseButton={true}
>
{this.state.hasError ?
<Callout
className={styles.errorCallout}
role={'alertdialog'}
gapSpace={0}
target={this.saveButtonElement}
onDismiss={() => this.setState({hasError: false})}
setInitialFocus={true}
>
<div>
<p className={styles.header}>
An error has occurred
</p>
</div>
<div className={styles.content}>
<p>
{this.state.error}
</p>
</div>
</Callout> :
null}
{/* For each lookup field in loaded item create a TagPicker component */}
{this.state.loadedItem.lookupMetadata.map((lm, i) => {
let selected = [];
if (lm.lookupValues != null && lm.lookupValues.length > 0) {
for (let val of lm.lookupValues) {
const filterResult = lm.allLookupValues.filter(v => v.lookupId == val.lookupId);
if (filterResult != null && filterResult.length > 0) {
selected.push({ key: filterResult[0].lookupId.toString(), name: unescapeHTML(filterResult[0].lookupValue)});
}
}
}
return (
<div style={{padding: "10px", minWidth: "400px", color: "white"}}>
<Label>{lm.lookupFieldDisplayName}</Label>
<TagPicker className={styles.tagPicker}
itemLimit={lm.lookupFieldIsMultiValue ? 100 : 1}
pickerSuggestionsProps={{
suggestionsHeaderText: 'Suggested items',
noResultsFoundText: 'No matches found',
}}
onResolveSuggestions={(filter, selectedItems) => this.resolveTagSuggestions(filter, selectedItems, lm.allLookupValues)}
defaultSelectedItems={selected}
onChange={(items?: ITag[]) => this.processTagItemsChange(items == null ? [] : items, lm.lookupFieldInternalName)}
/>
</div>);
})}
<DialogFooter>
<Button text='Cancel' title='Cancel' onClick={this.props.close} disabled={this.state.submitting} />
<span ref={(btn) => this.saveButtonElement = btn}>
<PrimaryButton
text='Save' title='Save' disabled={this.state.submitting}
onClick={() => this.submit(this.state.loadedItem)} />
</span>
</DialogFooter>
</DialogContent>)
);
}
// Update the state information to reflect the UI change in tag picker
@autobind
private processTagItemsChange(items: ITag[], fieldInternalName: string) {
for (let lm of this.state.loadedItem.lookupMetadata) {
if (lm.lookupFieldInternalName == fieldInternalName) {
lm.lookupValues = items.map(item => {
return {
lookupId: parseInt(item.key),
lookupValue: item.name
} as ILookupFieldValue;
});
break;
}
}
this.setState({ loadedItem: {...this.state.loadedItem}}, () => console.log(this.state.loadedItem));
}
// Return suggestons for a picker
@autobind
private resolveTagSuggestions(filterText: string, selectedItems: ITag[], allItems: ILookupFieldValue[]): ITag[] {
let results = [];
if (filterText) {
results = allItems
.filter(item => item.lookupValue != null && item.lookupValue.toLowerCase().indexOf(filterText.toLowerCase()) === 0)
.filter(item => !this.listContainsDocument({ key: item.lookupId.toString(), name: item.lookupValue }, selectedItems))
.map(r => {
return {
key: r.lookupId.toString(),
name: unescapeHTML(r.lookupValue)
} as ITag;
});
}
return results;
}
private listContainsDocument(tag: ITag, tagList: ITag[]) {
if (!tagList || !tagList.length || tagList.length === 0) {
return false;
}
return tagList.filter(compareTag => compareTag.key === tag.key).length > 0;
}
// Load current site pages item
// As well as load all items from all lookup lists
// Takes longer to load, but faster to fill once loaded
private loadItem() {
this.setState({loading: true});
const sitePagesName = 'Site Pages';
let refinerFieldInfos: IMetadataRefinerInfo[] = [];
// Load fields information from SitePages list
pnp.sp.web.lists.getByTitle(sitePagesName).fields.filter("ReadOnlyField eq false and Hidden eq false and substringof('Lookup',TypeAsString)")
.select("Title", "InternalName", "LookupList", "TypeAsString")
.get().then((res: any[]) => {
for (let f of res) {
if (!refinerFieldInfos.some(ri => ri.InternalName == f.InternalName)) {
refinerFieldInfos.push({
IsSelected: false,
DisplayName: f.Title,
InternalName: f.InternalName,
IsMultiValue: f.TypeAsString == 'Lookup' ? false : true,
List: f.LookupList
});
}
}
// Load all items from lookup lists
this.loadItemInternal(refinerFieldInfos).then(loadedItem => {
let allDataRetrievalPromises: Promise<any[]>[] = [];
for (let refinerInfo of refinerFieldInfos) {
let lookupMetadataItem = loadedItem.lookupMetadata.filter(lm => lm.lookupFieldInternalName == refinerInfo.InternalName)[0];
if (lookupMetadataItem != null) {
let promise = pnp.sp.web.lists.getById(lookupMetadataItem.lookupFieldLookupList).items.getAll();
allDataRetrievalPromises.push(promise);
promise.then(results => {
lookupMetadataItem.allLookupValues = results.map(result => {
return {
lookupId: result.ID,
lookupValue: result.Title
} as ILookupFieldValue;
});
});
}
}
Promise.all(allDataRetrievalPromises).then((promiseResults: any[][]) => {
this.setState({
loading: false,
loadedItem
});
}).catch(this.handleError);
});
}).catch(this.handleError);
}
// Load all items from lookup lists
private loadItemInternal(refinerFieldInfos: IMetadataRefinerInfo[]) {
let promise = new Promise<IMetadataNewsItem>((resolve, reject) => {
const noOfLookupsBeforeThrottle = 6;
let noOfRequests = Math.floor(refinerFieldInfos.length / noOfLookupsBeforeThrottle) + 1;
let loadPromises: Promise<any>[] = [];
let loadedItem = {
id: this.props.itemId,
content: null,
lookupMetadata: []
} as IMetadataNewsItem;
for (let i = 0; i < noOfRequests; i++) {
let numerOfRefinersToGet = noOfLookupsBeforeThrottle;
if (numerOfRefinersToGet + i * noOfLookupsBeforeThrottle > refinerFieldInfos.length) {
numerOfRefinersToGet = refinerFieldInfos.length % noOfLookupsBeforeThrottle;
}
let getFrom = 0;
let getTo = (i + 1) * numerOfRefinersToGet;
if (i > 0) {
getFrom = i * noOfLookupsBeforeThrottle - 1;
}
// Load the target item from SitePages list
let currentRefiners = refinerFieldInfos.slice(getFrom, getTo);
let p = this.constructNewsItemRequest(currentRefiners);
loadPromises.push(p);
p.then(item => {
loadedItem.title = item.Title;
for (let refinerInfo of currentRefiners) {
let lookupMetadataItem: ILookupInfo = null;
if (loadedItem.lookupMetadata.some(lm => lm.lookupFieldInternalName == refinerInfo.InternalName)) {
lookupMetadataItem = loadedItem.lookupMetadata.filter(lm => lm.lookupFieldInternalName == refinerInfo.InternalName)[0];
} else {
lookupMetadataItem = {
lookupFieldInternalName: refinerInfo.InternalName,
lookupFieldDisplayName: refinerInfo.DisplayName,
lookupFieldIsMultiValue: refinerInfo.IsMultiValue,
lookupFieldLookupList: refinerInfo.List,
lookupValues: [],
allLookupValues: []
};
loadedItem.lookupMetadata.push(lookupMetadataItem);
}
// TODO: ensure multilookup works as well
if (item[refinerInfo.InternalName] != null) {
lookupMetadataItem.lookupValues.push({
lookupId: item[refinerInfo.InternalName].Id,
lookupValue: item[refinerInfo.InternalName].Value
});
}
}
});
}
Promise.all(loadPromises).then(() => {
resolve(loadedItem);
});
});
return promise;
}
// Load target item from SitePages, return Promise object
private constructNewsItemRequest(ris: IMetadataRefinerInfo[]) {
const sitePagesName = 'Site Pages';
let toSelect: string[] = ["Title", "Author/Name", "Editor/Name", "Author/Title", "Editor/Title"];
let toExpand: string[] = ["Author", "Editor"];
ris.forEach(ri => {
toSelect.push(`${ri.InternalName}/Title`);
toSelect.push(`${ri.InternalName}/Id`);
toExpand.push(ri.InternalName);
});
let promise = pnp.sp.web.lists.getByTitle(sitePagesName).items.getById(this.props.itemId).select(...toSelect).expand(...toExpand).get();
return promise;
}
// Save state information to a list item
@autobind
private submit(itemInfo: IMetadataNewsItem): void {
this.setState({submitting: true});
let updateInfo = {};
for (let lm of itemInfo.lookupMetadata) {
if (lm.lookupValues == null || lm.lookupValues.length < 1) {
updateInfo[`${lm.lookupFieldInternalName}Id`] = null;
} else {
let values = lm.lookupValues.map(v => v.lookupId);
if (lm.lookupFieldIsMultiValue) {
updateInfo[`${lm.lookupFieldInternalName}Id`] = {results: values};
} else {
updateInfo[`${lm.lookupFieldInternalName}Id`] = values[0];
}
}
}
pnp.sp.web.lists.getByTitle('Site Pages').items.getById(itemInfo.id).update(updateInfo).then((res: pnp.ItemUpdateResult) => {
this.props.close();
}).catch(error => {
this.setState({
error: `There was a problem updating metadata.
Inner error: ${error.data.responseBody["odata.error"].message.value}`,
hasError: true,
submitting: false
});
});
}
private handleError(error: any) {
console.log(error);
this.setState({loading: false});
}
}
// Container component for dialog content
export default class FillMetadataDialog extends BaseDialog {
public itemId: number;
public webUrl: string;
public render(): void {
ReactDOM.render(<FillMetadataDialogContent
webUrl={this.webUrl}
close={this.close}
itemId={this.itemId}
submit={this.submit}
/>, this.domElement);
}
public getConfig(): IDialogConfiguration {
return {
isBlocking: false
};
}
@autobind
private submit(): void {
this.close();
}
}

View File

@ -0,0 +1,25 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/command-set-extension-manifest.schema.json",
"id": "d4fd389a-c02f-41ac-977a-474020daa4c6",
"alias": "MetadataSitePagesCommandSet",
"componentType": "Extension",
"extensionType": "ListViewCommandSet",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"items": {
"FILL_METADATA": {
"title": { "default": "Fill Metadata" },
"iconImageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfiAwwQNgwGoqtvAAAFqElEQVRo3u3YfWyVVx3A8U+hUFCEdryEMdzKgI1UMYhIdMFF5ggvyYZotjGIbswXmPEl6F6EdEj2ooiLkz8mdVlkixkwA2ZbjCvbjHWiRgEFYSbAWMvAAS2FQl0rXbnHP+5De+/1tn2eu9sYE3/PP895fuf8zvf8fuf5nReockro1+eCu/UgA/AJY/SvDHZzT6pSlOBVdxS506ddr9ozWOphJb0BQLuGIgO0o1kDmnurNqDI3SaW/wOUZpWGWBu75U8d8mkfy6NZ61+FA9wfu2WdQ+ZZnkezrnCAC34Su+WbeDWv5kL87nMB2q1I0thmmxPV7xOg1I1Z5dccM8G1CW2+orNQgGFezCrfrcZtvpcQoEJLoQApR7PKrTiX861vSSWpnA1wXuV/1NhoY0KARPJfT0TZAOUx1/dK1PaoLf+f8kDuHJgQq9Vx3GFoD9rzhQOkEuwKTvWHB95rexFsftbbhQIMMrcIAIMK90Bbz7vXBNJWOECHmiIAJJJsgDIrY7d8Vr25PpxH81jWgjzEDEzwbUGtfflMrRDUIn4iCoJ5qOklEdUKvokvZWiOoExZbyF4NrYHTmBX3qzX0fX2qHFO401/VG6ukabaYaD5/pLfA8WTAXZFo35LsBXTBO9oEgRnTM/vgYGmdr0f0Wq8UQV0vl+wyQwpP7fY5diGg/abapT9gg95xY3dXsg/B3qOcV9PuZWClM/hTk+4DhUGqvAN9xhtpL2Cf6QHn+2B4FzX+ztoyyjHl6ARJSbjKduts9X7tdmr2m8w0Rg0u9ifc+AhQYdSleozfJOyCt8VvGF8d+ViAzwZdXdamTpBvZuNMMUTgotmWR3pT6nKBRihpdcn3u7wrCDocJfZgrdN7NI8JdhhlL9GCMtzAfpKRC2xAFoEC4zGvYJtGZoZgtMYrNJvBStyJ2Fr3tTaLRfFlbc0oUL27UAzhivVoUF7+lM2wEV7k4c8rzzumHvtxSeVdh1U5uCATl+3wEe6Kxd7Eh6MAvYnY5wRPG4wmKlZUG1RV0gXvRuAKhvUqFHjsYxpBpMsj1LYeywRBA22qNMp2GOQDYI3rLDIwMIAyn0A2zIm55OoMqyrxrUaBbvBLU531dviMtwiCNYXGoIpTgi+plZQa506wVbVgoauc9UjgpRpGOQK5RZYZZmpRqhAeqlu7966JQFIdx+ktEb/8f2C89EILyFUOiY4aLYTgn2uANXaBQ9bKwgeKMQD6e4P2yQIjroM4yMXb3QsA2GSkzkhmpyVUR7KNBsfYKfgpPFKLLDUMCNdp0K5z5uDSVoEv4zqpo9uh/1YUGeerwjORaN/JNtsfID1gpS7wGz7o2Ds9lGwUhCszgDYZqz7MsZ9Bre6PddsXIAhrvayYCe+KhUtOEHQ6XYcEPxCZfTX16ZTrSkOCdo0qPed/IbjAbxPYzSOzSZrEzxtNC63TXDWOL+K9EcNzgDow3780/FEo8Fr1viyoercqQknLLZHuWXu8zq40rjYVhMez88p8UGvm4lnhOhrp62Y6YDJPd+KFwfgkpTJ3Hyn38sKspQQYLgGLxhlFxZmfF+IXcZ5Kfmlf3yA4zqUuMpNHvQzKZ/xgFIM9gM36LDFo+a4Cq2a+gPgtCnm+zWm+7P1eFCjP2hyD9b4u+l43jxVSe4HkoSgXpVP4TmsssxZFT5uuEa3+n70faHxjvdPCGAxTtqIKoeNNcNS01zplGvwQ+dwWyKLOVuyvmSlWmPtsMMa7Ha9Pcr9zTWC1ZYY4WyCC/8uSbIcz9Ia5buLgi/gW9F7zqGz+JkwLTvN90/8yMu4wQqzsN0mnM08cMaVZCFII0wz0UuexxJLQLsves6+xNfaBQFwxBGsdyn7tdsg5YUCLBUIkJbfZ+XCguUSwNA8F/XvToZipEqM7Kvq8oKuIZI9L/bU+QD8TmORR58rHT3PkH8DVdD9lPrQjicAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTgtMDMtMTJUMTY6NTQ6MTIrMDE6MDDGtVUHAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE4LTAzLTEyVDE2OjU0OjEyKzAxOjAwt+jtuwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAASUVORK5CYII=",
"type": "command"
}
}
}

View File

@ -0,0 +1,49 @@
import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import {
BaseListViewCommandSet,
Command,
IListViewCommandSetListViewUpdatedParameters,
IListViewCommandSetExecuteEventParameters
} from '@microsoft/sp-listview-extensibility';
import { Dialog } from '@microsoft/sp-dialog';
import * as strings from 'MetadataSitePagesCommandSetStrings';
import FillMetadataDialog from './FillMetadataDialog';
const LOG_SOURCE: string = 'MetadataSitePagesCommandSet';
export default class MetadataSitePagesCommandSet extends BaseListViewCommandSet<{}> {
@override
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, 'Initialized MetadataSitePagesCommandSet');
return Promise.resolve();
}
@override
public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
const fillMetadataCommand: Command = this.tryGetCommand('FILL_METADATA');
if (fillMetadataCommand) {
// This command should be hidden unless exactly one row is selected.
fillMetadataCommand.visible = event.selectedRows.length === 1;
}
}
@override
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
switch (event.itemId) {
case 'FILL_METADATA':
const dialog: FillMetadataDialog = new FillMetadataDialog();
dialog.webUrl = this.context.pageContext.web.absoluteUrl;
dialog.itemId = parseInt(event.selectedRows[0].getValueByName("ID"));
dialog.show().then(() => {
// console.log('after dialog close');
});
break;
default:
throw new Error('Unknown command');
}
}
}

View File

@ -0,0 +1,6 @@
define([], function() {
return {
"Command1": "Command 1",
"Command2": "Command 2"
}
});

View File

@ -0,0 +1,9 @@
declare interface IMetadataSitePagesCommandSetStrings {
Command1: string;
Command2: string;
}
declare module 'MetadataSitePagesCommandSetStrings' {
const strings: IMetadataSitePagesCommandSetStrings;
export = strings;
}

View File

@ -0,0 +1,113 @@
import { IContextualMenuItem } from '@microsoft/office-ui-fabric-react-bundle'; //'office-ui-fabric-react/lib';
import { PagedItemCollection } from '@pnp/sp';
import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
export interface IFillMetadataDialogContentProps {
itemId: number;
webUrl: string;
close: () => void;
submit: () => void;
}
export interface IFillMetadataDialogContentState {
loading: boolean;
submitting: boolean;
hasError: boolean;
error: string;
loadedItem: IMetadataNewsItem;
}
export interface IMetadataNewsProps {
ItemLimit: number;
ItemHeight: string;
AdditionalFilter: string;
HideRefinerFromItemCard: string;
RefinerInfos: IMetadataRefinerInfo[];
webUrl: string;
containerWidth: string;
multiColumn: boolean;
}
export interface IMetadataRefinerInfo {
IsSelected: boolean;
List?: string;
IsMultiValue: boolean;
DisplayName: string;
InternalName: string;
DefaultValues?:string;
}
export interface IMetadataNewsItem {
id: number;
bannerImg: string;
title: string;
content: string;
url: string;
created: string;
lookupMetadata: ILookupInfo[];
}
export interface ILookupInfo {
lookupFieldInternalName: string;
lookupFieldDisplayName: string;
lookupFieldLookupList: string;
lookupFieldIsMultiValue: boolean;
lookupValues: ILookupFieldValue[];
allLookupValues?: ILookupFieldValue[];
}
export interface ILookupFieldValue {
lookupId: number;
lookupValue: string;
}
export interface ILoadNewsResult {
pagedItems: PagedItemCollection<any>;
infos: IMetadataNewsItem[];
}
export interface IMetadataNewsPageCollectionInfo {
collection: PagedItemCollection<any>;
relatedRefiners: IMetadataRefinerInfo[];
}
export interface IMetadataNewsState {
currentNewsItems: IMetadataNewsItem[];
currentRefiners: IContextualMenuItem[];
currentPage: number;
pagedCollectionInfos: IMetadataNewsPageCollectionInfo[];
loading: boolean;
dialogItem: IMetadataNewsItem;
containerWidth: number;
containerHeight: number;
}
export interface IRefinersState {
nodes: IContextualMenuItem[];
loading: boolean;
error: string;
enabledFilterNodes: IContextualMenuItem[];
}
export interface IMetadataContextualMenuItemResult {
refinerName: string;
refinerFieldInternalName: string;
items: IContextualMenuItem[];
}
const escapeChars = { lt: '<', gt: '>', quot: '"', apos: "'", amp: '&' };
export const unescapeHTML = (str) => {
return str.replace(/\&([^;]+);/g, (entity, entityCode) => {
var match;
if ( entityCode in escapeChars) {
return escapeChars[entityCode];
} else if ( match = entityCode.match(/^#x([\da-fA-F]+)$/)) {
return String.fromCharCode(parseInt(match[1], 16));
} else if ( match = entityCode.match(/^#(\d+)$/)) {
return String.fromCharCode(~~match[1]);
} else {
return entity;
}
});
};

View File

@ -0,0 +1,28 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "07db8cfc-508d-4826-9a9c-9bf725350e56",
"alias": "MetadataNewsWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f71", // Other
"group": { "default": "Other" },
"title": { "default": "Metadata-News" },
"description": { "default": "Metadata-enabled news pages rollup" },
"officeFabricIconFontName": "News",
"properties": {
"description": "Metadata-enabled news pages rollup",
"containerWidth": "900",
"multiColumn": false
}
}]
}

View File

@ -0,0 +1,187 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneChoiceGroup,
PropertyPaneCheckbox,
PropertyPaneCustomField,
PropertyPaneFieldType,
PropertyPaneHorizontalRule,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import * as strings from 'MetadataNewsWebPartStrings';
import MetadataNews from './components/MetadataNews';
import { IMetadataNewsProps } from '../../interfaces';
import * as pnp from '@pnp/sp';
import { override } from '@microsoft/decorators';
export default class MetadataNewsWebPart extends BaseClientSideWebPart<IMetadataNewsProps> {
private lookupsFetched: boolean;
protected onInit(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.properties.ItemLimit === undefined) {
this.properties.ItemLimit = 5;
this.properties.ItemHeight = "180";
this.properties.RefinerInfos = [];
this.properties.AdditionalFilter = null;
this.properties.HideRefinerFromItemCard = null;
}
if (this.properties.containerWidth === undefined) {
this.properties.containerWidth = "900";
}
if (this.properties.multiColumn === undefined) {
this.properties.multiColumn = false;
}
pnp.sp.setup({
sp: {
headers: {
Accept: 'application/json;odata=verbose'
},
baseUrl: this.context.pageContext.web.absoluteUrl
}
});
this.properties.webUrl = this.context.pageContext.web.absoluteUrl;
resolve();
});
}
@override
public onDispose() {
super.onDispose();
}
public render(): void {
const element: React.ReactElement<IMetadataNewsProps> = React.createElement(MetadataNews, this.properties);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
// Get the property pane configuration
// During first opening we will fetch all lookup fields
// In order for fields to become visible we have to change the value of any of initially visible fields, like Item limit, or Item height
// Please refer to Chris O'Brien's article http://www.sharepointnutsandbolts.com/2016/09/sharepoint-framework-spfx-web-part-properties-dynamic-dropdown.html
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
if (!this.lookupsFetched) {
this.lookupsFetched = true;
pnp.sp.web.lists.getByTitle("Site Pages").fields.filter("ReadOnlyField eq false and Hidden eq false and substringof('Lookup',TypeAsString)").get().then((res: any[]) => {
for (let f of res) {
if (!this.properties.RefinerInfos.some(ri => ri.InternalName == f.InternalName)) {
this.properties.RefinerInfos.push({
IsSelected: false,
DisplayName: f.Title,
InternalName: f.InternalName,
IsMultiValue: f.TypeAsString == 'Lookup' ? false : true,
DefaultValues: '',
List: f.LookupList
});
}
}
this.onDispose();
}).catch(error => {
console.log(error);
});
}
let config = {} as IPropertyPaneConfiguration;
config.pages = [
{
header: {
description: strings.PropertyPaneDescription
},
displayGroupsAsAccordion: true,
groups: [
{
groupName: "General",
groupFields: [
PropertyPaneTextField('ItemLimit', {
label: 'Item limit',
value: this.properties.ItemLimit.toString(),
onGetErrorMessage: (text) => this.validateItemLimit(text)
}),
PropertyPaneHorizontalRule(),
PropertyPaneTextField('ItemHeight', {
label: 'Item height (px)',
value: this.properties.ItemHeight,
onGetErrorMessage: (text) => this.validateItemHeight(text)
}),
PropertyPaneHorizontalRule(),
PropertyPaneTextField('AdditionalFilter', {
label: 'Additional filter',
value: this.properties.AdditionalFilter,
}),
PropertyPaneHorizontalRule(),
PropertyPaneTextField('HideRefinerFromItemCard', {
label: 'Hide this refiner (internal name) value from item card',
value: this.properties.HideRefinerFromItemCard,
}),
PropertyPaneHorizontalRule(),
PropertyPaneTextField('containerWidth', {
label: 'Width of container in px',
value: this.properties.containerWidth,
}),
PropertyPaneHorizontalRule(),
PropertyPaneToggle('multiColumn', {
label: 'Show items in multiple columns'
})
]
},
{
groupName: "Refiner fields",
groupFields: []
}
]
}
];
if (this.lookupsFetched) {
for (let infoIndex in this.properties.RefinerInfos) {
config.pages[0].groups[1].groupFields.push(PropertyPaneCheckbox(`RefinerInfos[${infoIndex}].IsSelected`, { text: this.properties.RefinerInfos[infoIndex].DisplayName}));
config.pages[0].groups[1].groupFields.push(PropertyPaneTextField(`RefinerInfos[${infoIndex}].DefaultValues`, { description: "; delimited refiner values", value: this.properties.RefinerInfos[infoIndex].DefaultValues}));
config.pages[0].groups[1].groupFields.push(PropertyPaneHorizontalRule());
}
}
return config;
}
private validateItemLimit(text: string) {
const errMsg = 'Value must be numeric and between 1 and 100';
if (text == null || text == '') {
return errMsg;
} else {
let number = parseInt(text);
if (number.toString() != text || number < 1 || number > 100) {
return errMsg;
}
}
return '';
}
private validateItemHeight(text: string) {
const errMsg = 'Value must be numeric and between 150 and 500';
if (text == null || text == '') {
return errMsg;
} else {
let number = parseInt(text);
if (number.toString() != text || number < 150 || number > 500) {
return errMsg;
}
}
return '';
}
}

View File

@ -0,0 +1,195 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
@mixin accentColoring {
background-color: $ms-color-themeDark !important;
color: white !important;
}
@mixin accentColoringDarker {
background-color: $ms-color-themeDarker !important;
color: white !important;
}
@media all and (max-width: 600px) {
.dialogMainOverride {
min-width: 95%;
}
}
@media all and (min-width: 601px) {
.dialogMainOverride {
min-width: 85%;
}
}
.tagPicker {
div[class*="ms-TagItem "] {
background-color: #130707;
}
}
.dialogMainOverride {
background-color: white;
}
.errorCallout {
padding: 10px;
max-width: 450px;
.header {
font-size: 20px;
}
.content {
border-top: 1px solid $ms-color-themeDarker;
}
}
#newsContainer {
position: inherit;
}
.docCardActivity {
div[class*="docCardContents"] {
text-overflow: "ellipsis";
overflow: "hidden";
margin-left: 16px;
margin-right: 16px;
}
div[class*="ms-DocumentCardActivity-avatars"] {
display: none;
}
div[class*="ms-DocumentCardActivity-details"] {
left: 0;
position: relative;
}
}
.menuItem {
@include accentColoring();
> button {
@include accentColoring();
&:hover {
@include accentColoringDarker();
}
i[class*="itemChevronDown"], i[class*="itemChevronUp"] {
color: white !important;
}
}
> button.is-expanded {
@include accentColoringDarker();
}
&:hover, .is-expanded {
@include accentColoringDarker();
}
}
.metadataNews {
.commandBar {
@include accentColoring();
}
.container {
max-width: 900px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
padding: 10px;
}
.docCards {
> div {
background-color: white !important;
color: black !important;
margin: 0 auto;
height: inherit;
max-width: 980px;
> div > div {
// height: inherit;
max-height: inherit;
max-width: inherit;
}
> div + div {
height: inherit;
max-height: inherit;
margin: 0 auto;
div[class*="ms-DocumentCardTitle"], span[class*="ms-DocumentCardActivity-name"] {
color: #1f1f1f;
}
span[class*="ms-DocumentCardActivity-activity"] {
color: #908989;
}
div[class*="ms-DocumentCardActivity activity_"] {
padding-top: 3px;
padding-bottom: 7px;
}
}
}
}
.column {
@include ms-Grid-col;
@include ms-sm12;
@include ms-lg12;
@include ms-xl12;
// @include ms-xlPush1;
// @include ms-lgPush1;
> div > span > div {
margin: 0 auto !important;
padding-bottom: 10px;
}
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,530 @@
import * as React from 'react';
import styles from './MetadataNews.module.scss';
import { IMetadataNewsProps, IMetadataNewsState, IMetadataRefinerInfo, IMetadataNewsItem, ILookupInfo, ILoadNewsResult,
IMetadataNewsPageCollectionInfo, unescapeHTML, IMetadataContextualMenuItemResult } from '../../../interfaces';
import MetadataNewsRefiners from './MetadataNewsRefiners';
import { escape } from '@microsoft/sp-lodash-subset';
import { sp, PagedItemCollection } from '@pnp/sp';
import { IContextualMenuItem, ActionButton, Label, Spinner, SpinnerSize,
Dialog, DialogType, DialogFooter, PrimaryButton, Image, ImageFit } from
'@microsoft/office-ui-fabric-react-bundle';
//'office-ui-fabric-react/lib';
import { DocumentCard, DocumentCardActivity, DocumentCardPreview, DocumentCardTitle, DocumentCardType } from
'@microsoft/office-ui-fabric-react-bundle';
//'office-ui-fabric-react/lib/DocumentCard';
import Truncate from 'react-truncate';
// array.from shim for some versions of IE
let from = require('array.from');
if (Array['from'] === undefined) {
Array['from'] = from.shim();
}
// Plugin used for neatly arranging news items in the multiple column mode
import { CSSGrid, measureItems, layout } from 'react-stonecutter';
// measureItems is a higher order function that wraps CSSGrid component and returns another component (also grid, but that takes into account actual item image heights)
const Grid = measureItems(CSSGrid, { measureImages: true });
// A simple wrapper used in the MetadataNews component - conditionally injects Grid if multi column mode is enabled
class Wrapper extends React.Component<any,any> {
constructor(props: any, state: any) {
super(props);
}
public render() {
return this.props.multiColumn ?
<div style={{display: this.props.itemCount > 0 ? "block" : "none", margin: "0 auto"}}>
<Grid component="div" columns={this.props.columns} gutterWidth={10} gutterHeight={10} columnWidth={this.props.columnWidth} layout={layout.pinterest} >
{this.props.children}
</Grid>
</div> :
<div style={{display: this.props.itemCount > 0 ? "block" : "none", margin: "0 auto"}}>
{this.props.children}
</div>;
}
}
// Main component
export default class MetadataNews extends React.Component<IMetadataNewsProps, IMetadataNewsState> {
private containerElement: HTMLElement = null;
private themeBackgroundColor: string = null;
private wrapper: React.ClassicComponentClass<{}>;
constructor(props: IMetadataNewsProps, state: IMetadataNewsState) {
super(props);
this.state = {
currentNewsItems: [],
currentRefiners: [],
pagedCollectionInfos: null,
currentPage: 1,
loading: false,
dialogItem: null,
containerWidth: 0,
containerHeight: 0
};
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
sp.setup({
sp: {
headers: {
Accept: 'application/json;odata=verbose'
},
baseUrl: this.props.webUrl
}
});
}
public shouldComponentUpdate(nextProps: IMetadataNewsProps, nextState: IMetadataNewsState) {
return JSON.stringify(this.props) != JSON.stringify(nextProps) ||
JSON.stringify(this.state) != JSON.stringify(nextState);
}
// To make component render quickly the actual data retrieval starts after initial render in componentDidMount event
public componentDidMount() {
this.updateWindowDimensions();
window.addEventListener('resize', this.updateWindowDimensions);
// Waiting for __themeState__ variable to become available to correctly read the theme configuration (if present) and then loading the data
let reTryCount = 10;
let intervalId = setInterval(() => {
if ((window["__themeState__"] !== null && window["__themeState__"].theme !== null) || reTryCount < 1) {
this.themeBackgroundColor = window["__themeState__"].theme ?
window["__themeState__"].theme.accent :
"#0078d7";
clearInterval(intervalId);
this.loadNews();
}
reTryCount--;
}, 200);
}
// Removing event handler
public componentWillUnmount() {
window.removeEventListener('resize', this.updateWindowDimensions);
}
// Rendering logic
public render(): React.ReactElement<IMetadataNewsProps> {
let startItemsPosition = (this.state.currentPage - 1) * this.props.ItemLimit;
let endItemsPosition = startItemsPosition + parseInt(this.props.ItemLimit as any);
let docType = DocumentCardType.compact;
if (this.state.containerWidth < 600) {
docType = DocumentCardType.normal;
}
let itemMaxWidthNumber = parseInt(this.props.ItemHeight) * 1.7;
let itemMaxWidth = `inherit`;
let columnCount = 1;
if (this.props.multiColumn) {
columnCount = Math.floor(this.state.containerWidth / itemMaxWidthNumber);
docType = DocumentCardType.normal;
itemMaxWidth = `${itemMaxWidthNumber}px`;
}
return (
<div className={styles.metadataNews}>
<div className={styles.container} id={styles.newsContainer} style={{maxWidth: `${this.props.containerWidth}px`}}>
<MetadataNewsRefiners {...this.props} themeBackgroundColor={this.themeBackgroundColor} onContextualItemClick={(filterNodes) => this.handleRefinerChange(filterNodes)} />
<div className={styles.row}>
<div className={styles.column}>
{/* Paging */}
<div style={{margin: "0 auto", textAlign: "center"}}>
{
this.state.currentPage > 1 ?
<ActionButton data-automation-id='pagePrev' iconProps={{iconName: 'PageLeft'}}
onClick={() => {
if (this.state.currentPage == 2) {
this.setState({pagedCollectionInfos: null, currentPage: 1, currentNewsItems: []}, () => {
this.loadNews();
});
} else {
this.changePage(false);
}
}}></ActionButton>
: null
}
<Label style={{display: "inline-block", lineHeight: "40px", padding: 0, color: "#908989"}}>Page {this.state.currentPage}</Label>
{
this.state.pagedCollectionInfos != null && this.state.pagedCollectionInfos.length > 0 && this.state.pagedCollectionInfos[0].collection.hasNext ?
<ActionButton data-automation-id='pageNext' iconProps={{iconName: 'PageRight'}} onClick={() => this.changePage(true)}></ActionButton>
: null
}
</div>
{/* Shows the dialog component if user clicked on an item */}
{this.state.dialogItem == null ? null :
<Dialog
isOpen={true}
hidden={false}
onDismiss={() => this.setState({dialogItem: null})}
dialogContentProps={{
type: DialogType.largeHeader,
title: this.state.dialogItem.title,
responsiveMode: 5, //or ResponsiveMode.xxxLarge,
showCloseButton: true,
className: styles.dialogMainOverride
}}
modalProps={{
isBlocking: false,
responsiveMode: 5, //or ResponsiveMode.xxxLarge,
containerClassName: styles.dialogMainOverride,
firstFocusableSelector: styles.dialogMainOverride
}}
>
{this.state.dialogItem.bannerImg != null ?
<Image alt="" src={this.state.dialogItem.bannerImg} style={{maxWidth: "100%", margin: "0 auto"}}/>
: null}
<div className={styles.dialogMainOverride} dangerouslySetInnerHTML={{__html: this.state.dialogItem.content}}></div>
<DialogFooter><PrimaryButton onClick={() => this.setState({dialogItem: null})} text='Close' /></DialogFooter>
</Dialog>
}
{this.state.loading ?
<Spinner size={SpinnerSize.large} label='Loading...' ariaLive='assertive' />
:
<Wrapper itemCount={this.state.currentNewsItems.length} columns={columnCount} multiColumn={this.props.multiColumn} columnWidth={itemMaxWidthNumber}>
{
this.state.currentNewsItems.map((n: IMetadataNewsItem, i) => {
if (i < startItemsPosition || i >= endItemsPosition) {
return null;
}
return (
/* Below is the template for each rendered news item */
<div style={{margin: "10px", minHeight: `${this.props.ItemHeight}px`, maxWidth: itemMaxWidth}} className={styles.docCards} >
{/* Reusing... or rather brutally abusing the DocumentCard and related components to instead display news item content in them */}
<DocumentCard type={docType}
onClick={() => { this.onItemClick(n); }} >
{/* Image rendering code */}
{n.bannerImg == null || n.bannerImg == "" ? null :
<div style={{maxWidth: `${parseInt(this.props.ItemHeight) * 1.7}px`}}>
<DocumentCardPreview previewImages={[{
url: n.bannerImg,
name: "...",
previewImageSrc: n.bannerImg,
height: parseInt(this.props.ItemHeight),
imageFit: ImageFit.cover,
errorImageSrc: "/_layouts/images/prvnews.gif"
}]} />
</div>
}
{/* Title and news item body rendering code */}
<div style={{width: `calc(100% - ${n.bannerImg == null || n.bannerImg == "" || docType == DocumentCardType.normal ? 0 : parseInt(this.props.ItemHeight) * 1.7}px)`}}>
<DocumentCardTitle
title={n.title}
shouldTruncate={true}
/>
<div className={styles.docCardActivity}>
<div className="docCardContents">
<Truncate lines={4} ellipsis={<span>...</span>}>
<p dangerouslySetInnerHTML={{__html: this.extractContents(n.content)}}></p>
</Truncate>
</div>
<DocumentCardActivity
activity={n.lookupMetadata != null ? unescapeHTML(this.combinePostMetadata(n.lookupMetadata)) : ''}
people={[{name: n.created.toString(), profileImageSrc: null}]}
/>
</div>
</div>
</DocumentCard>
</div>
);
})
}
</Wrapper>
}
</div>
</div>
</div>
</div>
);
}
// Join the titles of lookup field values from multiple lookup fields for a single item
private combinePostMetadata(lookupInfos: ILookupInfo[]): string {
let toReturn = '';
let combined = lookupInfos.map(ni => {
let vals = ni.lookupValues.map(v => v.lookupValue);
let res = [];
for (let v of vals) {
if (v != null) {
res.push(v);
}
return res.length == 0 ? null : res.join('; ');
}
});
combined = combined.filter((n) => n != undefined );
toReturn = combined.join('; ');
return toReturn;
}
// Make main container dimensions-aware
private updateWindowDimensions() {
this.containerElement = window.document.getElementById(styles.newsContainer);
if (this.containerElement != null) {
this.setState({
containerWidth: this.containerElement.offsetWidth,
containerHeight: this.containerElement.offsetHeight
});
}
}
private onItemClick(item: IMetadataNewsItem) {
this.setState({dialogItem: item});
}
// Paging logic - change page number and call the loadNews method when going forward
private changePage(toNextPage: boolean) {
if (toNextPage && this.state.currentNewsItems.length >= this.props.ItemLimit) {
this.setState({currentPage: this.state.currentPage + 1}, () => {
let itemsLimit = this.state.currentPage * this.props.ItemLimit;
if (this.state.currentNewsItems.length < itemsLimit) {
this.loadNews();
}
});
} else if (this.state.currentPage > 1) {
this.setState({currentPage: this.state.currentPage - 1});
}
}
// Compose SharePoint REST API filter string
private getRestFilter() {
let filter = "ContentType eq 'Site Page' and PromotedState eq 2";
filter += this.props.AdditionalFilter != null && this.props.AdditionalFilter.length > 0 ? ` and ${this.props.AdditionalFilter}` : "";
// sort the filters into { internalName, [values] } objects
let sortedRefiners = [];
for (let item of this.state.currentRefiners) {
if (!sortedRefiners.some(val => val.internalName == item.data)) {
sortedRefiners.push({ internalName: item.data, values: [item.name]});
} else {
let info = sortedRefiners.filter(it => it.internalName == item.data);
if (info != null && info.length > 0) {
let itemInfo = info[0];
itemInfo.values.push(item.name);
}
}
}
for (let itemInfo of sortedRefiners) {
for (let i = 0; i < itemInfo.values.length; i++) {
let currentFilterPart = `${itemInfo.internalName}/Title eq '${encodeURIComponent(itemInfo.values[i]).replace("'",'%27%27')}'`;
if (filter.length == 0) {
filter = currentFilterPart;
} else {
filter += ` and ${currentFilterPart}`;
}
}
}
//console.log(`filter: ${filter}`);
//filter = encodeURI(filter);
//console.log(`filter encoded: ${filter}`);
return filter;
}
// Build the pnp news request and return Promise object
private constructNewsRequest(ris: IMetadataRefinerInfo[]) {
let listName = "Site Pages";
let toSelect: string[] = ["ID", "CanvasContent1", "BannerImageUrl", "Created", "Title", "FileRef", "FileLeafRef",
"Author/Name", "Editor/Name", "Author/Title", "Editor/Title"];
let toExpand: string[] = ["Author", "Editor"];
ris.forEach(ri => {
toSelect.push(`${ri.InternalName}/Title`);
toSelect.push(`${ri.InternalName}/Id`);
toExpand.push(ri.InternalName);
});
let promise: Promise<PagedItemCollection<any>> = sp.web.lists.getByTitle(listName).items.filter(this.getRestFilter())
.select(...toSelect).expand(...toExpand).orderBy("Created", false).top(this.props.ItemLimit).getPaged();
return promise;
}
// Main news getting request logic
private processNewsRequest(promise: Promise<PagedItemCollection<any>>, ris: IMetadataRefinerInfo[], allNewsItems: IMetadataNewsItem[]) {
let infosPromise: Promise<ILoadNewsResult> = new Promise<ILoadNewsResult>((resolve, reject) => {
let toResolve: ILoadNewsResult = {
infos: [],
pagedItems: null
};
promise.then((val: PagedItemCollection<any>) => {
for (let item of val.results) {
let newsItem = null;
for (let ni of allNewsItems) {
if (ni.id == item.ID) {
newsItem = ni;
break;
}
}
// Build a JSON object holding relevant list item data
if (newsItem == null) {
newsItem = {
id: item.ID,
bannerImg: item.BannerImageUrl != null ? item.BannerImageUrl.Url : "",
title: item.Title,
url: item.FileRef,
content: item.CanvasContent1.replace("data-sp-componentid", "style=\"display: none;\" data-sp-componentid"),
created: item.Created.split('T')[0]
} as IMetadataNewsItem;
toResolve.infos.push(newsItem);
}
// Supplement
ris.forEach(ri => {
if (item[ri.InternalName] != null && ri.InternalName != this.props.HideRefinerFromItemCard) {
if (newsItem.lookupMetadata == null) {
newsItem.lookupMetadata = [];
}
let lookupMetadataItem: ILookupInfo;
if (newsItem.lookupMetadata.some(lm => lm.lookupFieldInternalName == ri.InternalName)) {
lookupMetadataItem = newsItem.lookupMetadata.filter(lm => lm.lookupFieldInternalName == ri.InternalName)[0];
} else {
lookupMetadataItem = {
lookupFieldInternalName: ri.InternalName,
lookupFieldDisplayName: ri.DisplayName,
lookupFieldIsMultiValue: ri.IsMultiValue,
lookupFieldLookupList: null,
lookupValues: []
};
newsItem.lookupMetadata.push(lookupMetadataItem);
}
lookupMetadataItem.lookupValues.push({
lookupId: item[ri.InternalName].Id,
lookupValue: item[ri.InternalName].Title
});
}
});
}
toResolve.pagedItems = val;
resolve(toResolve);
}).catch(err => {
console.error(err);
resolve(toResolve);
});
});
return infosPromise;
}
// This method gets called when actual requests are needed
private loadNews() {
this.setState({loading: true});
let allItemInfos: IMetadataNewsItem[] = [];
let pagedItemCollections: IMetadataNewsPageCollectionInfo[] = [];
let loadPromises: Promise<PagedItemCollection<any>>[] = [];
// This branch handles subsequent requests
if (this.state.pagedCollectionInfos != null) {
for (let pagePartInfo of this.state.pagedCollectionInfos) {
let promise = pagePartInfo.collection.getNext();
loadPromises.push(promise);
this.processNewsRequest(promise, pagePartInfo.relatedRefiners, allItemInfos).then(res => {
allItemInfos.push(...res.infos);
if (res.pagedItems != null) {
pagedItemCollections.push({
collection: res.pagedItems,
relatedRefiners: pagePartInfo.relatedRefiners
});
}
});
}
}
// Ths branch handles initial news item get request
// In the event of lookup column count exceeding the throttling limit we need to make several requests to get all of lookup values
else {
const noOfLookupsBeforeThrottle = 6;
let noOfRequests = Math.floor(this.props.RefinerInfos.length / noOfLookupsBeforeThrottle) + 1;
for (let i = 0; i < noOfRequests; i++) {
let numerOfRefinersToGet = noOfLookupsBeforeThrottle;
if (numerOfRefinersToGet + i * noOfLookupsBeforeThrottle > this.props.RefinerInfos.length) {
numerOfRefinersToGet = this.props.RefinerInfos.length % noOfLookupsBeforeThrottle;
}
let getFrom = 0;
let getTo = (i + 1) * numerOfRefinersToGet;
if (i > 0) {
getFrom = i * noOfLookupsBeforeThrottle - 1;
}
let currentRefiners = this.props.RefinerInfos.slice(getFrom, getTo);
let promise = this.constructNewsRequest(currentRefiners);
loadPromises.push(promise);
this.processNewsRequest(promise, currentRefiners, allItemInfos).then(res => {
allItemInfos.push(...res.infos);
if (res.pagedItems != null) {
pagedItemCollections.push({
collection: res.pagedItems,
relatedRefiners: currentRefiners
});
}
});
}
}
// When all requests are complete - set the state, making sure currentNewsItems are extended by newly received data
Promise.all(loadPromises).then(() => {
this.setState({
currentNewsItems: [...this.state.currentNewsItems, ...allItemInfos],
loading: false,
pagedCollectionInfos: pagedItemCollections.length > 0 ? pagedItemCollections : null
});
});
}
// React to refiner change in the MetadataNewsRefiners component
private handleRefinerChange(incomingRefiners: IContextualMenuItem[]) {
this.setState({
currentRefiners: incomingRefiners,
currentPage: 1,
pagedCollectionInfos: null,
currentNewsItems: []
}, () => {
this.loadNews();
});
}
// Helper method that trims any unwanted information from body of the news item
private extractContents(s: string | Element) {
let el: Element = null;
if (s instanceof Element) {
el = s;
} else {
el = document.createElement('span');
el.innerHTML = s;
}
let res = null;
if (el.hasAttribute('data-sp-rte')) {
res = el.textContent || el.innerHTML;
} else {
for (let i = 0; i < el.children.length; i++) {
let child = el.children.item(i);
res = this.extractContents(child);
if (res != null) {
break;
}
}
}
return res;
}
}

View File

@ -0,0 +1,250 @@
import * as React from 'react';
import styles from './MetadataNews.module.scss';
import { IMetadataNewsProps, IMetadataRefinerInfo, IRefinersState, IMetadataContextualMenuItemResult, unescapeHTML } from '../../../interfaces';
import { IContextualMenuItem, CommandBar } from
//'@microsoft/office-ui-fabric-react-bundle';
'office-ui-fabric-react/lib';
import { sp } from '@pnp/sp';
import { cloneDeep } from '@microsoft/sp-lodash-subset';
export interface IMetadataNewsRefinerProps extends IMetadataNewsProps {
onContextualItemClick(currentRefiners: IContextualMenuItem[]);
themeBackgroundColor: string;
}
export default class MetadataNewsRefiners extends React.Component<IMetadataNewsRefinerProps, IRefinersState> {
constructor(props: IMetadataNewsRefinerProps, state: IRefinersState) {
super(props);
this.state = {
nodes: null,
loading: true,
error: null,
enabledFilterNodes: []
};
sp.setup({
sp: {
headers: {
Accept: 'application/json;odata=verbose'
},
baseUrl: this.props.webUrl
}
});
}
// To make component render quickly the actual data retrieval starts after initial render in componentDidMount event
public componentDidMount() {
if (this.state.nodes == null) {
this.loadData();
}
}
// Rendering logic
public render(): React.ReactElement<IMetadataNewsProps> {
let currentItems: IContextualMenuItem[] = null;
if (this.state.loading) {
currentItems = [{
key: "loading",
name: "Loading...",
isDisabled: false,
itemType: 0 // ContextualMenuItemType.Normal
}];
} else {
currentItems = this.state.nodes;
}
const needToShowFilter = this.state.enabledFilterNodes.length > 0;
return (
this.state.nodes != null && this.state.nodes.length > 0 ?
<div className={styles.row}>
<div className={styles.column}>
<div>
<CommandBar style={{backgroundColor: this.props.themeBackgroundColor}} isSearchBoxVisible={false}
items={currentItems}
farItems={[
{
className: "ms-bgColor-neutral",
key: "clear",
name: "Clear",
iconProps: {
iconName: 'RemoveFilter'
},
onClick: () => this.removeFilter()
}
]}
/>
<div style={{display: needToShowFilter ? "block" : "none", color: "#908989"}}>
<span>Filters: </span>
{
this.state.enabledFilterNodes.map((n, i) => {
return (<span style={{fontStyle: "italic"}} >{(i == 0 ? "" : ", ") + n.name}</span>);
})
}
</div>
</div>
</div>
</div>
: null
);
}
// This method gets called when actual requests are needed
private loadData() {
this.setState({ loading: true });
// Construct refiner / filter data requests
let promises: Promise<IMetadataContextualMenuItemResult>[] = [];
if (this.props.RefinerInfos != null) {
for (let info of this.props.RefinerInfos) {
if (info.IsSelected) {
promises.push(this.tryLoadRefiner(info));
}
}
}
// Build items from SharePoint lookup lists data
Promise.all(promises).then((results: IMetadataContextualMenuItemResult[]) => {
let items: IContextualMenuItem[] = [];
results.forEach((res, i) => {
if (res != null && res.items != null && res.items.length > 0) {
let topItem: IContextualMenuItem = {
key: i.toString(),
name: res.refinerName,
data: res.refinerFieldInternalName,
items: res.items
};
items.push(topItem);
}
});
this.setState({ loading: false, nodes: items });
});
}
// Load refiner items for a particular field in Site Pages
// If semicolon-delimited list of values is specified in the WebPart properties - use it
// Otherwise make a REST call to SharePoint to get all items in needed list
private tryLoadRefiner(refinerInfo: IMetadataRefinerInfo): Promise<IMetadataContextualMenuItemResult> {
let deferred = new Promise<IMetadataContextualMenuItemResult>((resolve, reject) => {
let toResolve: IMetadataContextualMenuItemResult = {
refinerName: refinerInfo.DisplayName,
refinerFieldInternalName: refinerInfo.InternalName,
items: null
};
// This branch gets called when WebPart has semicolon-delimited values for this field
if (refinerInfo.DisplayName != null && refinerInfo.IsSelected) {
if (refinerInfo.DefaultValues != null && refinerInfo.DefaultValues.length > 0) {
let refiners = refinerInfo.DefaultValues.split(';');
toResolve.items = refiners.map((r) => {
return {
items: null,
itemType: 0, // or ContextualMenuItemType.Normal,
data: refinerInfo.InternalName,
checked: false,
isChecked: false,
canCheck: true,
key: r,
name: r,
onClick: (a, b) => this.localHandleItemClick(a, b)
} as IContextualMenuItem;
});
resolve(toResolve);
}
// This branch gets called when we need to request data from lookup list
else {
let promise = null;
if (refinerInfo.List === undefined) {
const webSiteRelativeUrl = this.props.webUrl.replace(window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : ''), "");
const listRelativeUrl = `${webSiteRelativeUrl}/${refinerInfo.InternalName}`;
promise = sp.web.getList(listRelativeUrl).items.orderBy('Title', true).usingCaching().getAll();
} else {
promise = sp.web.lists.getById(refinerInfo.List).items.orderBy('Title', true).usingCaching().getAll();
}
promise.then(items => {
toResolve.items = items.map((i) => {
const title = unescapeHTML(i.Title);
return {
items: null,
itemType: 0, // or ContextualMenuItemType.Normal,
data: refinerInfo.InternalName,
checked: false,
isChecked: false,
canCheck: true,
key: title,
name: title,
onClick: (a, b) => this.localHandleItemClick(a, b)
} as IContextualMenuItem;
});
resolve(toResolve);
}).catch(e => {
console.error(e);
resolve(toResolve);
});
}
}
});
return deferred;
}
// Called when user clicks on one of refiners and triggers filtering
// enabledFilterNodes is an array of selected filter values with strict AND relationship (filter on Value1 AND Value2 AND ... etc)
private localHandleItemClick(ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) {
let cloned = cloneDeep<IContextualMenuItem[]>(this.state.enabledFilterNodes);
if (item.isChecked) {
item.isChecked = false;
let index = 0;
for (let i = 0; i < cloned.length; i++) {
if (cloned[i].key == item.key) {
index = i;
break;
}
}
cloned.splice(index, 1);
} else {
item.isChecked = true;
cloned.push(item);
}
this.setState({ enabledFilterNodes: cloned });
this.forceUpdate();
this.props.onContextualItemClick(cloned);
}
// Called from "Clear" command button
private removeFilter() {
this.removeFilterRecursive(this.state.nodes);
this.setState({ enabledFilterNodes: []});
this.props.onContextualItemClick([]);
this.forceUpdate();
}
private removeFilterRecursive(nodes: IContextualMenuItem[]) {
if (nodes != null) {
for (let n of nodes) {
n.checked = n.isChecked = false;
this.removeFilterRecursive(n.items);
}
}
}
private getItemByKey(nodes: IContextualMenuItem[], key: string): IContextualMenuItem {
let item = null;
if (nodes != null) {
for (let n of nodes) {
if (n.key == key) {
item = n;
} else {
item = this.getItemByKey(n.items, key);
}
if (item != null) {
break;
}
}
}
return item;
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Configuration parameters for this webpart",
"BasicGroupName": "Web part properteis",
"MetadataFieldsLabel": "Metadata field internal names, separated by ;"
}
});

View File

@ -0,0 +1,10 @@
declare interface IMetadataNewsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
MetadataFieldsLabel: string;
}
declare module 'MetadataNewsWebPartStrings' {
const strings: IMetadataNewsWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}