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:
parent
6899e6d7fe
commit
6a59355c8b
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.4.1",
|
||||
"libraryName": "react-sitepages-metadata",
|
||||
"libraryId": "46c3c432-396d-4e9b-944d-6bf8c5466d0c",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"Command1": "Command 1",
|
||||
"Command2": "Command 2"
|
||||
}
|
||||
});
|
9
samples/react-sitepages-metadata/src/extensions/pagesMetadata/loc/myStrings.d.ts
vendored
Normal file
9
samples/react-sitepages-metadata/src/extensions/pagesMetadata/loc/myStrings.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
declare interface IMetadataSitePagesCommandSetStrings {
|
||||
Command1: string;
|
||||
Command2: string;
|
||||
}
|
||||
|
||||
declare module 'MetadataSitePagesCommandSetStrings' {
|
||||
const strings: IMetadataSitePagesCommandSetStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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 '';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Configuration parameters for this webpart",
|
||||
"BasicGroupName": "Web part properteis",
|
||||
"MetadataFieldsLabel": "Metadata field internal names, separated by ;"
|
||||
}
|
||||
});
|
10
samples/react-sitepages-metadata/src/webparts/metadataNews/loc/mystrings.d.ts
vendored
Normal file
10
samples/react-sitepages-metadata/src/webparts/metadataNews/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare interface IMetadataNewsWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
MetadataFieldsLabel: string;
|
||||
}
|
||||
|
||||
declare module 'MetadataNewsWebPartStrings' {
|
||||
const strings: IMetadataNewsWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue