Merge pull request #2411 from PathToSharePoint/main

New sample React-Cherry-Picked-Content
This commit is contained in:
Hugo Bernier 2022-03-28 00:49:37 -04:00 committed by GitHub
commit 9a53c78c55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 48507 additions and 0 deletions

View File

@ -0,0 +1,39 @@
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
{
"name": "SPFx 1.14.0",
"image": "docker.io/m365pnp/spfx:1.14.0",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
4321,
35729
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
// Not needed for SPFx>= 1.12.1
// "5432": {
// "protocol": "https",
// "label": "Workbench",
// "onAutoForward": "silent"
// },
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -0,0 +1,33 @@
echo
echo -e "\e[1;94mInstalling Node dependencies\e[0m"
npm install
## commands to create dev certificate and copy it to the root folder of the project
echo
echo -e "\e[1;94mGenerating dev certificate\e[0m"
gulp trust-dev-cert
# Convert the generated PEM certificate to a CER certificate
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
# Copy the PEM ecrtificate for non-Windows hosts
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
## add *.cer to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.cer' ./.gitignore
then
echo "# .CER Certificates" >> .gitignore
echo "*.cer" >> .gitignore
fi
## add *.pem to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.pem' ./.gitignore
then
echo "# .PEM Certificates" >> .gitignore
echo "*.pem" >> .gitignore
fi
echo
echo -e "\e[1;92mReady!\e[0m"
echo -e "\n\e[1;94m**********\nOptional: if you plan on using gulp serve, don't forget to add the container certificate to your local machine. Please visit https://aka.ms/spfx-devcontainer for more information\n**********"

View File

@ -0,0 +1,37 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
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
# .CER Certificates
*.cer
# .PEM Certificates
*.pem

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Hosted workbench",
"type": "pwa-chrome",
"request": "launch",
"url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222",
"-incognito"
]
}
]
}

View File

@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
// Configure glob patterns for excluding files and folders in the file explorer.
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bower_components": true,
"**/coverage": true,
"**/lib-amd": true,
"src/**/*.scss.ts": true
},
"typescript.tsdk": ".\\node_modules\\typescript\\lib"
}

View File

@ -0,0 +1,16 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"version": "1.14.0",
"libraryName": "react-cherry-picked-content",
"libraryId": "6a0a135e-c421-4508-955a-dbb0b392104a",
"environment": "spo",
"packageManager": "npm",
"solutionName": "react-cherry-picked-content",
"solutionShortDescription": "react-cherry-picked-content description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,101 @@
# Cherry picked content
## Summary
The Cherry-Picked Content Web Part is a modern replacement for the classic Content Editor Web Part, with a twist: code snippets can only be picked from approved document libraries.
![React Cherry=Picked Content Sample](./assets/React-Cherry-Picked-Content-Sample.png)
## Compatibility
![SPFx 1.14](https://img.shields.io/badge/SPFx-1.14-green.svg)
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
Start by editing the `ApprovedLibraries.ts` file to list your approved libraries. Then upload your code snippets to those locations. If you are looking for ideas, check out the samples folder.
The code can be rendered in two ways:
- isolated: the code is wrapped in an `iframe` to prevent conflicts with other Web Parts. Note: this is not a security feature.
- non isolated: the code is directly inserted in the page.
## Solution
Solution|Author(s)
--------|---------
React-Cherry-Picked-Content | [Christophe Humbert](https://github.com/PathToSharePoint)
## Version history
Version|Date|Comments
-------|----|--------
0.3.0|March 9, 2022|4 samples added
0.2.0|March 6, 2022|Refactoring
0.1.0|February 21, 2022|Initial draft
## Minimal path to awesome
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-cherry-picked-content) then unzip it)
- From your command line, change your current directory to the directory containing this sample (`react-cherry-picked-content`, located under `samples`)
- Under components, edit `ApprovedLibraries.ts` to list your approved libraries that contain HTML snippets
- Upload the code snippets
- Ensure that you are at the solution folder
- in the command-line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts:
- Cascading dropdown and conditional display in the Property Pane
- Use of `SPHttpClient` and the SharePoint REST API to query SharePoint content
- React function component with `useState` and `useEffect` hooks
- React Portal in combination with an `iframe`
- Various examples of client-side code in the samples: Microsoft Graph (teams), Microsoft Graph Toolkit (people, email, agenda), charts (Chart.js, Chartist), widgets (map, stock, countdown, clock, video, game), SharePoint SOAP and REST APIs.
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
## Help
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-cherry-picked-content%22) to see if anybody else is having the same issues.
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-cherry-picked-content) and see what the community is saying.
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-cherry-picked-content&template=bug-report.yml&sample=react-cherry-picked-content&authors=@PathToSharePoint&title=react-cherry-picked-content%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-cherry-picked-content&template=question.yml&sample=react-cherry-picked-content&authors=@PathToSharePoint&title=react-cherry-picked-content%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-cherry-picked-content&template=suggestion.yml&sample=react-cherry-picked-content&authors=@PathToSharePoint&title=react-cherry-picked-content%20-%20).
## 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.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-cherry-picked-content" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,50 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-cherry-picked-content",
"source": "pnp",
"title": "Cherry picked content",
"shortDescription": "The Cherry-Picked Content Web Part is a modern replacement for the classic Content Editor Web Part, with a twist: code snippets can only be picked from approved document libraries.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-cherry-picked-content",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-cherry-picked-content",
"longDescription": [
"The Cherry-Picked Content Web Part is a modern replacement for the classic Content Editor Web Part, with a twist: code snippets can only be picked from approved document libraries."
],
"creationDateTime": "2022-02-21",
"updateDateTime": "2022-03-09",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.14"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-cherry-picked-content/assets/React-Cherry-Picked-Content-Sample.png",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "PathToSharePoint",
"pictureUrl": "https://github.com/PathToSharePoint.png",
"name": "Christophe Humbert"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"cherry-picked-content-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/cherryPickedContent/CherryPickedContentWebPart.js",
"manifest": "./src/webparts/cherryPickedContent/CherryPickedContentWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"CherryPickedContentWebPartStrings": "lib/webparts/cherryPickedContent/loc/{locale}.js"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-cherry-picked-content",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-cherry-picked-content-client-side-solution",
"id": "6a0a135e-c421-4508-955a-dbb0b392104a",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "Christophe Humbert",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.14.0"
},
"metadata": {
"shortDescription": {
"default": "react-cherry-picked-content description"
},
"longDescription": {
"default": "react-cherry-picked-content description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "react-cherry-picked-content Feature",
"description": "The feature that activates elements of the react-cherry-picked-content solution.",
"id": "bb64f400-c482-4467-83ae-3a05b9eacf4d",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/react-cherry-picked-content.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

View File

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

View File

@ -0,0 +1,16 @@
'use strict';
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.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{
"name": "react-cherry-picked-content",
"version": "0.3.0",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "16.13.1",
"react-dom": "16.13.1",
"office-ui-fabric-react": "7.174.1",
"@microsoft/sp-core-library": "1.14.0",
"@microsoft/sp-property-pane": "1.14.0",
"@microsoft/sp-webpart-base": "1.14.0",
"@microsoft/sp-lodash-subset": "1.14.0",
"@microsoft/sp-office-ui-fabric-core": "1.14.0"
},
"devDependencies": {
"@types/react": "16.9.51",
"@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.14.0",
"@microsoft/sp-tslint-rules": "1.14.0",
"@microsoft/sp-module-interfaces": "1.14.0",
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
"gulp": "~4.0.2",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1"
}
}

View File

@ -0,0 +1,79 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Chart.js Bubble Chart</h3>
<p>Isolated mode: &#9888; mandatory. Source: <a href="https://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/" target="_blank">Tobias Ahlin</a>.</p>
<div style="width:500px; height:300px; background-color:#f7f7f7;">
<canvas id="bubble-chart"></canvas>
<div style="padding:10px;">Source: </div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js" onload="drawBubble()"></script>
<script>
function drawBubble() {
new Chart(document.getElementById("bubble-chart"), {
type: 'bubble',
data: {
labels: "Africa",
datasets: [
{
label: ["China"],
backgroundColor: "rgba(255,221,50,0.2)",
borderColor: "rgba(255,221,50,1)",
data: [{
x: 21269017,
y: 5.245,
r: 15
}]
}, {
label: ["Denmark"],
backgroundColor: "rgba(60,186,159,0.2)",
borderColor: "rgba(60,186,159,1)",
data: [{
x: 258702,
y: 7.526,
r: 10
}]
}, {
label: ["Germany"],
backgroundColor: "rgba(0,0,0,0.2)",
borderColor: "#000",
data: [{
x: 3979083,
y: 6.994,
r: 15
}]
}, {
label: ["Japan"],
backgroundColor: "rgba(193,46,12,0.2)",
borderColor: "rgba(193,46,12,1)",
data: [{
x: 4931877,
y: 5.921,
r: 15
}]
}
]
},
options: {
title: {
display: true,
text: 'Predicted world population (millions) in 2050'
}, scales: {
yAxes: [{
scaleLabel: {
display: true,
labelString: "Happiness"
}
}],
xAxes: [{
scaleLabel: {
display: true,
labelString: "GDP (PPP)"
}
}]
}
}
});
}
</script>
</div>

View File

@ -0,0 +1,39 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Chartist Animated Lines</h3>
<p>Isolated mode: &#9888; mandatory. Source: <a href="https://gionkunz.github.io/chartist-js/" target="_blank">Chartist.js</a>.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chartist@0.11.4/dist/chartist.css">
<script src="https://cdn.jsdelivr.net/npm/chartist@0.11.4/dist/chartist.min.js" onload="drawChart()"></script>
<div class="ct-chart ct-perfect-fourth"></div>
</div>
<script>
function drawChart() {
var chart = new Chartist.Line('.ct-chart', {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
series: [
[1, 5, 2, 5, 4, 3],
[2, 3, 4, 8, 1, 2],
[5, 4, 3, 2, 1, 0.5]
]
}, {
low: 0,
showArea: true,
showPoint: false,
fullWidth: true
});
chart.on('draw', function(data) {
if(data.type === 'line' || data.type === 'area') {
data.element.animate({
d: {
begin: 2000 * data.index,
dur: 2000,
from: data.path.clone().scale(1, 0).translate(0, data.chartRect.height()).stringify(),
to: data.path.clone().stringify(),
easing: Chartist.Svg.Easing.easeOutQuint
}
});
}
});
}
</script>
</div>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Agenda Custom View - Microsoft Graph Toolkit</h3>
<p>Isolated mode: &#9888; mandatory. Find more samples on <a href="https://mgt.dev/?path=/story/overview--page" target="_blank">MGT Playground</a>. MGT components require API permissions, see the <a href="https://docs.microsoft.com/en-us/graph/toolkit/overview">Microsoft docs</a> for more info.</p>
<script>
function setProvider() {mgt.Providers.globalProvider = new mgt.SharePointProvider(props.context);}
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/wc/webcomponents-loader.js" type="text/javascript"></script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt.es6.js" type="text/javascript" onload="setProvider()"></script>
<mgt-person person-query="me" view="twoLines"></mgt-person>
<mgt-agenda></mgt-agenda>
<!-- <mgt-agenda show-max="7" days="10">
<template data-type="event">
<div class="root">
<div class="time-container">
<div class="date">{{ dayFromDateTime(event.start.dateTime)}}</div>
<div class="time">{{ timeRangeFromEvent(event, '12') }}</div>
</div>
<div class="separator">
<div class="vertical-line top"></div>
<div class="circle">
<div data-if="!event.bodyPreview.includes('Join Microsoft Teams Meeting')" class="inner-circle">
</div>
</div>
<div class="vertical-line bottom"></div>
</div>
<div class="details">
<div class="subject">{{ event.subject }}</div>
<div class="location" data-if="event.location.displayName">
at
<a href="https://bing.com/maps/default.aspx?where1={{event.location.displayName}}"
target="_blank"><b>{{ event.location.displayName }}</b></a>
</div>
<div class="attendees" data-if="event.attendees.length">
<span class="attendee" data-for="attendee in event.attendees">
<mgt-person person-query="{{attendee.emailAddress.name}}"></mgt-person>
</span>
</div>
<div class="online-meeting" data-if="event.bodyPreview.includes('Join Microsoft Teams Meeting')">
<img class="online-meeting-icon" src="https://img.icons8.com/color/48/000000/microsoft-teams.png" />
<a class="online-meeting-link" href="{{ event.onlineMeetingUrl }}">Join Teams Meeting</a>
</div>
</div>
</div>
</template>
</mgt-agenda> -->
</div>

View File

@ -0,0 +1,70 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Emails Custom View - Microsoft Graph Toolkit</h3>
<p>Isolated mode: &#9888; mandatory. Find more samples on <a href="https://mgt.dev/?path=/story/overview--page" target="_blank">MGT Playground</a>. MGT components require API permissions, see the <a href="https://docs.microsoft.com/en-us/graph/toolkit/overview">Microsoft docs</a> for more info.</p>
<style>
.email {
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
padding: 10px;
margin: 8px 4px;
font-family: Segoe UI, Frutiger, Frutiger Linotype, Dejavu Sans, Helvetica Neue, Arial, sans-serif;
}
.email:hover {
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.3);
padding: 10px;
margin: 8px 4px;
}
.email h3 {
font-size: 12px;
margin-bottom: 4px;
}
.email h4 {
font-size: 10px;
margin-top: 0px;
margin-bottom: 4px;
}
.email mgt-person {
--font-size: 10px;
--avatar-size-s: 12px;
}
.email .preview {
font-size: 13px;
text-overflow: ellipsis;
word-wrap: break-word;
overflow: hidden;
max-height: 2.8em;
line-height: 1.4em;
}
</style>
<script>
function setProvider() {mgt.Providers.globalProvider = new mgt.SharePointProvider(props.context);}
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/wc/webcomponents-loader.js" type="text/javascript"></script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt.es6.js" type="text/javascript" onload="setProvider()"></script>
<mgt-person person-query="me" view="twoLines"></mgt-person>
<mgt-get resource="/me/messages" version="beta" scopes="mail.read" max-pages="1">
<template>
<div class="email" data-for="email in value">
<h3>{{ email.subject }}</h3>
<h4>
<mgt-person person-query="{{email.sender.emailAddress.address}}" view="oneline" person-card="hover">
</mgt-person>
</h4>
<div data-if="email.bodyPreview" class="preview" innerHtml>{{email.bodyPreview}}</div>
<div data-else class="preview">
email body is empty
</div>
</div>
</template>
<template data-type="loading">
loading
</template>
<template data-type="error">
{{ this }}
</template>
</mgt-get>
</div>

View File

@ -0,0 +1,25 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>People Components - Microsoft Graph Toolkit</h3>
<p>Isolated mode: &#9888; mandatory. Find more samples on the <a href="https://mgt.dev/?path=/story/overview--page" target="_blank">MGT Playground</a>. MGT components require API permissions, see the <a href="https://docs.microsoft.com/en-us/graph/toolkit/overview">Microsoft docs</a> for more info.</p>
<script>
function setProvider() {mgt.Providers.globalProvider = new mgt.SharePointProvider(props.context);}
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/wc/webcomponents-loader.js" type="text/javascript">
</script>
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt.es6.js" type="text/javascript" onload="setProvider()">
</script>
<div style="display: flex;">
<div style="flex: 50%;">
<h4>mgt-login</h4>
<mgt-login></mgt-login>
<h4>mgt-person</h4>
<mgt-person person-query="me" view="twoLines"></mgt-person>
<h4>mgt-people</h4>
<mgt-people show-max="10"></mgt-people>
</div>
<div style="flex: 50%;">
<h4>mgt-person-card</h4>
<mgt-person-card person-query="me"></mgt-person-card>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>My Teams - MS Graph Client</h3>
<p>Isolated mode: &#9888; mandatory. This sample leverages the SPFx built-in Microsoft Graph client.</p>
<script>
var logoURL = "https://static2.sharepointonline.com/files/fabric-cdn-prod_20200430.002/assets/brand-icons/product/svg/teams_48x1.svg";
var currentScript = document.currentScript;
props.context.msGraphClientFactory
.getClient()
.then((client) => {
client
.api("me/joinedTeams")
.version("v1.0")
.get((err, res) => {
if (res) {
let items = res.value.map(team => `<li><img src="${logoURL}" style="width:24px;"/> ${team.displayName}</li>`);
const newDiv = document.createElement('div');
newDiv.innerHTML = `<div>You are a member of ${items.length} teams:</div><ul style="list-style-type: none; columns: 3;">${items.join("")}</ul>`;
currentScript.insertAdjacentElement('afterend', newDiv);
}
});
});
</script>
</div>

View File

@ -0,0 +1,16 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Site Lists - SharePoint REST API</h3>
<p>Isolated mode: &#9888; mandatory. This sample uses the JavaScript fetch method to query the SharePoint REST API.</p>
<script>
var currentScript = document.currentScript;
fetch(props.context.pageContext.web.absoluteUrl + "/_api/web/lists?$select=Title,ImageUrl", {headers: {'Accept': 'application/json'}})
.then(response => response.json())
.then(result => result.value)
.then(lists => lists.map(list => `<li><img src="${list.ImageUrl}"/> ${list.Title}</li>`))
.then(items => {
const newDiv = document.createElement('div');
newDiv.innerHTML = `<div>${items.length} lists found on this site:</div><ul style="list-style-type: none; columns: 2;">${items.join("")}</ul>`;
currentScript.insertAdjacentElement('afterend', newDiv);
});
</script>
</div>

View File

@ -0,0 +1,30 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>TradingView Widget</h3>
<p>Isolated mode: recommended. Source: <a href="https://www.tradingview.com/widget/" target="_blank">TradingView widget</a>.</p>
<!-- TradingView Widget BEGIN -->
<div class="tradingview-widget-container">
<div id="tradingview_cd1aa"></div>
<div class="tradingview-widget-copyright"><a href="https://www.tradingview.com/symbols/NASDAQ-MSFT/" rel="noopener" target="_blank"><span class="blue-text">MSFT Chart</span></a> by TradingView</div>
<script type="text/javascript" src="https://s3.tradingview.com/tv.js" onload="drawChart()"></script>
<script type="text/javascript">
function drawChart() {
new TradingView.widget(
{
"autosize": true,
"symbol": "NASDAQ:MSFT",
"interval": "D",
"timezone": "Etc/UTC",
"theme": "light",
"style": "1",
"locale": "en",
"toolbar_bg": "#f1f3f6",
"enable_publishing": false,
"allow_symbol_change": true,
"container_id": "tradingview_cd1aa"
}
);
}
</script>
</div>
<!-- TradingView Widget END -->
</div>

View File

@ -0,0 +1,92 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Site Lists - pocketSOAP</h3>
<p>Isolated mode: &#9888; mandatory. Just for fun, a minimal implementation of the deprecated SharePoint SOAP API. Do not use in production!</p>
<script type="text/javascript">
/********************************************************************************
pocketSOAP
Copyright (c) 2009-2022 Christophe Humbert
********************************************************************************/
pS = {};
pS.cleanSpaces = function (string) { return string.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " "); };
pS.cleanBreaks = function (string) { return string.replace(/[\r\n\t]/g, ""); };
pS.trim = function (string) { return string.replace(/^\s+|\s+$/g, ""); };
pS.htmlUnescape = function (str) {
return String(str)
.replace(/&quot;/g, '"')
.replace(/&#39;/g, '\'')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
};
pS.getWSProperties = function (site, service, operation) {
return fetch(site + "/" + '_vti_bin/' + service + '.asmx' + '?op=' + operation + '&_ts=' + new Date().getTime())
.then(result => result.text())
.then(text => {
var regexp = /Content-Type:([\s\S]*?)Content-Length:[\s\S]*?SOAPAction:\s*"(\S*?)"([\s\S]*?)\<\/pre\>/;
var results = regexp.exec(text);
let dv = document.createElement("div");
var spWS = {};
spWS.headers = {};
spWS.headers['Content-Type'] = pS.trim(results[1]).replace(/[\r\n]/g, "");
spWS.headers.SOAPAction = pS.trim(results[2]).replace(/[\r\n]/g, "");
spWS.soapEnvelope = results[3].replace(/&lt;xsd:schema&gt;[\s\S]+?&lt;\/xsd:schema&gt;/g, "");
spWS.soapEnvelope = spWS.soapEnvelope.replace(/&lt;([\S]+)&gt;[^;]+&lt;\/\1&gt;/g, '<$1/>');
spWS.soapEnvelope = pS.htmlUnescape(spWS.soapEnvelope);
// Trim and remove line breaks (\n,\r), tabs (\t), etc.
spWS.soapEnvelope = pS.trim(spWS.soapEnvelope).replace(/>\s+?</g, "><");
return spWS;
}); // end then
};
pS.soap = function (options) {
return pS.getWSProperties(options.site, options.service, options.operation)
.then(function (wsProperties) {
var wsURL = options.site + '/_vti_bin/' + options.service + '.asmx';
var wsData = wsProperties.soapEnvelope.replace(/<([\S]+?)\/>/g, function (m, key) {
if (options[key]) { return ('<' + key + '>' + options[key] + '</' + key + '>') }
else { return ""; }
});
options.method = 'POST'; // always POST for Web services
options.headers = wsProperties.headers;
options.body = wsData;
return fetch(wsURL, options);
}); // end then
}; // end pS.soap
</script>
<script type="text/javascript">
var currentScript = document.currentScript;
pS.soap({
// Service info
site: props.context.pageContext.web.absoluteUrl,
service: "Lists",
operation: "GetListCollection",
// Service parameters
viewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='ImageURL' /></ViewFields>"
})
.then(response => response.text())
.then(data => new DOMParser().parseFromString(data, "application/xml"))
.then(xml => {
const newDiv = document.createElement('div');
const items = [...xml.getElementsByTagName("List")].map(child => `<li><img src="${child.getAttribute("ImageUrl")}"/> ${child.getAttribute("Title")}</li>`);
newDiv.innerHTML = `<div>${items.length} lists found on this site:</div><ul style="list-style-type: none; columns: 2;">${items.join("")}</ul>`;
currentScript.insertAdjacentElement('afterend', newDiv);
});
</script>
</div>

View File

@ -0,0 +1,263 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Analog Clock</h3>
<p>Isolated mode: recommended.</p>
<p>Source: Tushar Nankani on <a href="https://github.com/tusharnankani/AnalogClock" target="_blank">Github</a>.</p>
</div>
<div class="clock">
<div class="hour">
<div class="hr" id="hr">
</div>
</div>
<div class="min">
<div class="mn" id="mn">
</div>
</div>
<div class="sec">
<div class="sc" id="sc">
</div>
</div>
</div>
<div class="toggleClass" onclick="toggleClass()"></div>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #090909;
background: #07141b;
}
body.light {
background: #d1dae3;
}
.clock {
width: 375px;
height: 375px;
display: flex;
justify-content: center;
align-items: center;
background: url(../clock.png);
background-size: cover;
border: 4px;
/* box-shadow: 15px 15px 15px rgba(255, 255, 255, 0.5); */
box-shadow: 0em -1.2em 1.2em rgba(255, 255, 255, 0.06),
inset 0em -1.2em 1.2em rgba(255, 255, 255, 0.06),
0em 1.2em 1.2em rgba(0, 0, 0, 0.3),
inset 0em 1.2em 1.2em rgba(0, 0, 0, 0.3);
border-radius: 50%;
}
body.light .clock {
box-shadow: 0em -1.2em 1.2em rgba(255, 255, 255, 0.3),
inset 1em 1em -1em rgba(255, 255, 255, 0.3),
0em -1.2em -1.2em rgba(0, 0, 0, 0.5),
inset 1em -1em 1em rgba(0, 0, 0, 0.1);
}
.clock :hover {
/* yet to be completed; when hovered, diplay complete information about time, `new Date().toLocaleString();` */
cursor: pointer;
}
/* The small circle int the center */
.clock:before {
content: '';
position: absolute;
width: 15px;
height: 15px;
background: rgb(255, 255, 255);
border-radius: 50%;
/* The z-index property specifies the stack order of an element.
/* An element with greater stack order is always in front of an element with a lower stack order. */
/* Note: z-index only works on positioned elements (position: absolute, position: relative, position: fixed, or position: sticky). */
z-index: 10000;
/* kept as a high value, since wanted at top */
}
body.light .clock:before {
background: #1a74be;
}
.clock .hour,
.clock .min,
.clock .sec {
position: absolute;
}
/* length of respective arms; */
.clock .hour, .hr {
width: 160px;
height: 160px;
}
.clock .min, .mn {
width: 190px;
height: 190px;
}
.clock .sec, .sc {
width: 230px;
height: 230px;
}
.hr, .mn, .sc {
display: flex;
justify-content: center;
/* align-items: center; */
position: absolute;
border-radius: 50%;
}
.hr:before {
content: '';
position: absolute;
width: 7.5px;
height: 80px;
background: #f81460;
z-index: 10;
/* z-index least */
border-radius: 2.8px;
}
.mn:before {
content: '';
position: absolute;
width: 3.5px;
height: 100px;
background: #ffffff;
z-index: 11;
/* z-index more than hour hand */
border-radius: 3px;
}
body.light .mn:before {
background: #091921;
}
.sc:before {
content: '';
position: absolute;
width: 2px;
height: 150px;
background: #0075fa80;
z-index: 12;
/* z-index more than hour minute hand */
border-radius: 3px;
}
.toggleClass {
position: absolute;
top: 35px;
right: 150px;
width: 20px;
margin: 2px;
height: 20px;
font-size: 18px;
border-radius: 50%;
background: #d1dae3;
color: #d1dae3;
font-family: 'Quicksand', sans-serif;
cursor: pointer;
display: flex;
align-items: center;
}
.toggleClass:before {
position: absolute;
content: 'Light Mode';
white-space: nowrap;
left: 25px;
}
body.light .toggleClass {
background: #091921;
color: #091921;
content: 'Dark Mode';
}
body.light .toggleClass:before {
content: 'Dark Mode';
white-space: nowrap;
}
</style>
<script>// For toggle button;
function toggleClass()
{
const body = document.querySelector('body');
body.classList.toggle('light');
body.style.transition = `0.3s linear`;
}
// for time;
const deg = 6;
// 360 / (12 * 5);
const hr = document.querySelector('#hr');
const mn = document.querySelector('#mn');
const sc = document.querySelector('#sc');
setInterval(() => {
let day = new Date();
let hh = day.getHours() * 30;
let mm = day.getMinutes() * deg;
let ss = day.getSeconds() * deg;
let msec = day.getMilliseconds();
// VERY IMPORTANT STEP:
hr.style.transform = `rotateZ(${(hh) + (mm / 12)}deg)`;
mn.style.transform = `rotateZ(${mm}deg)`;
sc.style.transform = `rotateZ(${ss}deg)`;
// gives the smooth transitioning effect, but there's a bug here!
// sc.style.transition = `1s`;
})
</script>

View File

@ -0,0 +1,14 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Page Banner</h3>
<p>Isolated mode: not recommended. Read more on the <a href="https://blog.pathtosharepoint.com/2020/10/26/a-temporary-message-on-top-of-your-sharepoint-page/" target="_blank">Path to SharePoint blog</a>.</p>
<style type="text/css">
body::before {
display:block;
width:100%;
background-color: DarkOrange;
font-size: 20px;
padding: 10px;
content: "Headed for the office? Remember to bring your badge!";
}
</style>
</div>

View File

@ -0,0 +1,187 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Flip Styled Countdown</h3>
<p>Isolated mode: recommended. Source: FlipDown on <a href="https://github.com/PButcher/flipdown" target="_blank">Github</a>.</p>
<div class="example">
<p>⏰ FlipDown.js, a lightweight flip styled countdown clock</p>
<div id="flipdown" class="flipdown"></div>
</div>
<link href="https://pbutcher.uk/flipdown/css/flipdown/flipdown.css" rel="stylesheet">
<style>
html {
height: 100%;
}
body {
margin: 0px;
height: 100%;
display: flex;
align-items: center;
align-content: space-around;
}
body,
.example h1,
.example p,
.example .button {
transition: all .2s ease-in-out;
}
body.light-theme {
background-color: #151515;
}
body.light-theme .example h1 {
color: #FFFFFF;
}
body.light-theme .example p {
color: #FFFFFF;
}
body.light-theme .buttons .button {
color: #FFFFFF;
border-color: #FFFFFF;
}
body.light-theme .buttons .button:hover {
color: #151515;
background-color: #FFFFFF;
}
.example {
font-family: 'Roboto', sans-serif;
width: 550px;
height: 378px;
margin: auto;
padding: 20px;
box-sizing: border-box;
}
.example .flipdown {
margin: auto;
}
.example h1 {
text-align: center;
font-weight: 100;
font-size: 3em;
margin-top: 0;
margin-bottom: 10px;
}
.example p {
text-align: center;
font-weight: 100;
margin-top: 0;
margin-bottom: 35px;
}
.example .buttons {
width: 100%;
height: 50px;
margin: 50px auto 0px auto;
display: flex;
align-items: center;
justify-content: space-around;
}
.example .buttons p {
height: 50px;
line-height: 50px;
font-weight: 400;
padding: 0px 25px 0px 0px;
color: #333;
margin: 0px;
}
.example .button {
display: inline-block;
height: 50px;
box-sizing: border-box;
line-height: 46px;
text-decoration: none;
color: #333;
padding: 0px 20px;
border: solid 2px #333;
border-radius: 4px;
text-transform: uppercase;
font-weight: 700;
transition: all .2s ease-in-out;
}
.example .button:hover {
background-color: #333;
color: #FFF;
}
.example .button i {
margin-right: 5px;
}
@media(max-width: 550px) {
.example {
width: 100%;
height: 362px;
}
.example h1 {
font-size: 2.5em;
}
.example p {
margin-bottom: 25px;
}
.example .buttons {
width: 100%;
margin-top: 25px;
text-align: center;
display: block;
}
.example .buttons p,
.example .buttons a {
float: none;
margin: 0 auto;
}
.example .buttons p {
padding-right: 0px;
}
.example .buttons a {
display: inline-block;
}
}
</style>
<script>
const runFlipDown = () => {
// Unix timestamp (in seconds) to count down to
var twoDaysFromNow = (new Date().getTime() / 1000) + (86400 * 2) + 1;
// Set up FlipDown
var flipdown = new FlipDown(twoDaysFromNow)
// Start the countdown
.start()
// Do something when the countdown ends
.ifEnded(() => {
console.log('The countdown has ended!');
});
// Toggle theme
var interval = setInterval(() => {
let body = document.body;
body.classList.toggle('light-theme');
body.querySelector('#flipdown').classList.toggle('flipdown__theme-dark');
body.querySelector('#flipdown').classList.toggle('flipdown__theme-light');
}, 5000);
var ver = document.getElementById('ver');
ver.innerHTML = flipdown.version;
};
</script>
<script src="https://pbutcher.uk/flipdown/js/flipdown/flipdown.js" onload="runFlipDown()"></script>
</div>

View File

@ -0,0 +1,7 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>HTML Sample</h3>
<p>Isolated mode: not recommended. Source: <a href="https://blog.pathtosharepoint.com/2021/07/29/introducing-the-property-pane-portal/" target="_blank">Path to SharePoint blog</a>.</p>
<p><i>This got me thinking. Could we in some way avoid the redundancy? And as a bonus, directly hook a regular Reactjs component in here? That&#8217;s what I pictured in the diagram below: a generic, reusable frame that could serve as host to any Reactjs control.</i></p>
<figure><img style="width:100%;" src="https://pathtosharepoint.files.wordpress.com/2021/07/image-3.png" /></figure>
<p><i>It took me a couple days to come up with a workable model (the &#8220;Property Pane Portal&#8221;), weeks to test it, and then months to start writing about it 😊. In the next episodes, I&#8217;ll share some samples of the PPP in action, and I&#8217;ll provide more details on the architecture and the code that supports it. The key ingredient is <a href="https://reactjs.org/docs/portals.html">Reactjs Portals</a>, which allow to beam an element to another part of the DOM.</i></p>
</div>

View File

@ -0,0 +1,5 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Google Maps</h3>
<p>Isolated mode: not recommended. Source: Google Maps embed code.</p>
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d85972.3820324134!2d-122.17073538560777!3d47.67204903850155!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x54900cad2000ee23%3A0x5e0390eac5d804f2!2sRedmond%2C%20WA!5e0!3m2!1sen!2sus!4v1645677983596!5m2!1sen!2sus" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy"></iframe>
</div>

View File

@ -0,0 +1,5 @@
<div style="background-color: CadetBlue; padding: 20px;">
<h3>Embedded YouTube Video</h3>
<p>Isolated mode: not required. Source: YouTube embed code.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/ozhbLz1gMi0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

View File

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

View File

@ -0,0 +1,32 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "3e86ecba-18ac-44e2-862e-fc268a929584",
"alias": "CherryPickedContentWebPart",
"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 approved.
// 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,
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
"supportsThemeVariants": true,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Cherry-Picked-Content" },
"description": { "default": "Cherry-Picked-Content description" },
"officeFabricIconFontName": "BullseyeTargetEdit",
"properties": {
"description": "Cherry-Picked-Content",
"isolated": true,
"iframeWidth": "100%",
"iframeHeight": "600px"
}
}]
}

View File

@ -0,0 +1,207 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
IPropertyPaneDropdownOption,
PropertyPaneCheckbox,
PropertyPaneDropdown,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import * as strings from 'CherryPickedContentWebPartStrings';
import CherryPickedContent from './components/CherryPickedContent';
import { ICherryPickedContentProps } from './components/ICherryPickedContentProps';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { approvedLibraries } from './components/ApprovedLibraries';
export interface ICherryPickedContentWebPartProps {
isolated: boolean;
iframeWidth: string;
iframeHeight: string;
description: string;
libraryPicker: string;
libraryItemPicker: string;
}
export default class CherryPickedContentWebPart extends BaseClientSideWebPart<ICherryPickedContentWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
protected onInit(): Promise<void> {
this._environmentMessage = this._getEnvironmentMessage();
return super.onInit();
}
public render(): void {
const element: React.ReactElement<ICherryPickedContentProps> = React.createElement(
CherryPickedContent,
{
description: this.properties.description,
libraryPicker: this.properties.libraryPicker,
libraryItemPicker: this.properties.libraryItemPicker,
approvedLibraries: this.approvedLibraries,
isolated: this.properties.isolated,
width: this.properties.iframeWidth,
height: this.properties.iframeHeight,
context: this.context,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName
}
);
ReactDom.render(element, this.domElement);
}
private _getEnvironmentMessage(): string {
if (!!this.context.sdks.microsoftTeams) { // running in Teams
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
}
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment;
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const {
semanticColors
} = currentTheme;
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText);
this.domElement.style.setProperty('--link', semanticColors.link);
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
// Only content from the approved libraries can be selected
private approvedLibraries = approvedLibraries;
// Dropdown gets disabled while retrieving items asynchronously
private itemsDropdownDisabled: boolean = true;
// Files in the selected library
private libraryItemsList: IPropertyPaneDropdownOption[];
// Asynchronous library query
private getLibraryItemsList = (library) => {
// Validate approved location
const filesLocation = this.approvedLibraries.filter(loc => loc.key == library)[0];
const filesQuery = window.location.origin + filesLocation.siteRelativeURL + "/_api/web/lists/getbytitle('" + filesLocation.library + "')/files?$select=Name";
return this.context.spHttpClient.get(filesQuery, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => response.json())
.then(data => data.value);
}
// Runs before getting the Property Pane configuration
protected onPropertyPaneConfigurationStart(): void {
this.itemsDropdownDisabled = true;
if (this.properties.libraryPicker)
this.getLibraryItemsList(this.properties.libraryPicker)
.then((files): void => {
// store items
this.libraryItemsList = files.map(file => file.Name).sort().map(name => { return { key: name, text: name }; });
this.itemsDropdownDisabled = false;
})
.then(() => this.context.propertyPane.refresh());
}
// This API is invoked after updating the new value of the property in the property bag (Reactive mode).
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
if ((propertyPath === 'libraryPicker') && (newValue)) {
// get previously selected item
const previousItem: string = this.properties.libraryItemPicker;
// reset selected item
this.properties.libraryItemPicker = "";
// disable item selector until new items are loaded
this.itemsDropdownDisabled = true;
// push new item value
this.onPropertyPaneFieldChanged('libraryItemPicker', previousItem, this.properties.libraryItemPicker);
// refresh the item selector control by repainting the property pane
this.context.propertyPane.refresh();
this.getLibraryItemsList(newValue)
.then((files): void => {
if (files.length) {
// store items
this.libraryItemsList = files.map(file => { return { key: file.Name, text: file.Name }; });
// enable item selector
this.itemsDropdownDisabled = false;
// refresh the item selector control by repainting the property pane
this.context.propertyPane.refresh();
}
});
}
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
// Web Part title
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
// Library Picker (approved libraries only)
PropertyPaneDropdown('libraryPicker', {
label: strings.LibraryPickerLabel,
options: this.approvedLibraries,
selectedKey: this.properties.libraryPicker
}),
// Cascading Library Item Picker
PropertyPaneDropdown('libraryItemPicker', {
label: strings.LibraryItemPickerLabel,
options: this.libraryItemsList,
selectedKey: this.properties.libraryItemPicker,
disabled: this.itemsDropdownDisabled
})
]
},
{
groupName: strings.IsolatedMode,
groupFields: [
// Isolated options
PropertyPaneCheckbox('isolated', {
text: strings.Isolated,
}),
this.properties.isolated && PropertyPaneTextField('iframeWidth', {
label: strings.IframeWidth
}),
this.properties.isolated && PropertyPaneTextField('iframeHeight', {
label: strings.IframeHeight
}),
]
}
]
}
]
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,20 @@
export const approvedLibraries = [
{
key: "/sites/PublicCDN/SiteAssets",
siteRelativeURL: "/sites/PublicCDN",
library: "Site Assets",
text: "Public CDN Site Assets"
},
{
key: "/sites/PublicCDN/Shared%20Documents",
siteRelativeURL: "/sites/PublicCDN",
library: "Documents",
text: "Public CDN Documents"
},
{
key: "/sites/PrivateCDN/SiteAssets",
siteRelativeURL: "/sites/PrivateCDN",
library: "Site Assets",
text: "Private CDN Site Assets"
}
];

View File

@ -0,0 +1,34 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.cherryPickedContent {
overflow: hidden;
padding: 1em;
color: "[theme:bodyText, default: #323130]";
color: var(--bodyText);
&.teams {
font-family: $ms-font-family-fallbacks;
}
}
.welcome {
text-align: center;
}
.welcomeImage {
width: 100%;
max-width: 420px;
}
.links {
a {
text-decoration: none;
color: "[theme:link, default:#03787c]";
color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only
&:hover {
text-decoration: underline;
color: "[theme:linkHovered, default: #014446]";
color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only
}
}
}

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import styles from './CherryPickedContent.module.scss';
import { ICherryPickedContentProps } from './ICherryPickedContentProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import PortalIframe from './PortalIframe';
const CherryPickedDiv = ({ htmlFragment }) =>
<div ref={ref => { if (ref) { ref.innerHTML = ""; ref.appendChild(document.createRange().createContextualFragment(htmlFragment)); } }}>
</div>;
const MemoDiv = React.memo(CherryPickedDiv);
const CherryPickedContent: React.FunctionComponent<ICherryPickedContentProps> = (props) => {
const message = "Loading...";
const [htmlFragment, setHtmlFragment] = React.useState(message);
// Get the file content
React.useEffect(() => {
async function fetchSnippet() {
// Validate that the library is in the approved list
let filteredApprovedLibraries = props.approvedLibraries.filter(lib => lib.key == props.libraryPicker);
if ((filteredApprovedLibraries.length > 0) && (props.libraryItemPicker)) {
let fileURL = props.libraryPicker + "/" + props.libraryItemPicker;
const webURLQuery = props.context.pageContext.web.absoluteUrl + `/_api/sp.web.getweburlfrompageurl(@v)?@v=%27${window.location.origin}${fileURL}%27`;
let webURL = await props.context.spHttpClient.get(webURLQuery, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => response.json())
.then(data => data.value);
const snippetURLQuery = webURL + `/_api/web/getFileByServerRelativeUrl('${fileURL}')/$value`;
const fragment = await props.context.spHttpClient.get(snippetURLQuery, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => response.text());
setHtmlFragment(fragment);
}
else {
setHtmlFragment(message);
}
}
fetchSnippet();
}, [props.libraryItemPicker]);
if (!props.libraryItemPicker) {
return (
<section className={`${styles.cherryPickedContent} ${props.hasTeamsContext ? styles.teams : ''}`}>
<div style={{ display: "flex" }}>
<div style={{ flex: "50%" }}>
<h3>Edit Web Part properties to select a file.</h3>
<h3>Approved libraries:</h3>
<p>
<ul>
{props.approvedLibraries.map(lib => <li>{lib.text}</li>)}
</ul>
</p>
</div>
<div style={{ flex: "50%" }} className={styles.welcome}>
<img alt="" src={props.isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} />
<h2>Welcome, {escape(props.userDisplayName)}!</h2>
<div>{props.environmentMessage}</div>
</div>
</div>
</section>
);
}
else if (props.isolated) {
return (
<PortalIframe {...props}>
<MemoDiv htmlFragment={htmlFragment} />
</PortalIframe>
);
}
else {
return (
<MemoDiv htmlFragment={htmlFragment} />
);
}
};
export default CherryPickedContent;

View File

@ -0,0 +1,16 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface ICherryPickedContentProps {
description: string;
libraryPicker: string;
libraryItemPicker: string;
approvedLibraries: any[];
isolated: boolean;
width: string;
height: string;
context: WebPartContext;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
}

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { createPortal } from 'react-dom';
const PortalIframe = ({
children,
...props
}) => {
const [contentRef, setContentRef] = React.useState(null);
const mountWindow = contentRef?.contentWindow;
const mountNode = contentRef?.contentWindow?.document?.body;
// Pass the props to the child iframe
if (mountWindow) mountWindow.props = props;
return (
<iframe width={props.width} height={props.height} style={{border:0}} ref={setContentRef}>
{mountNode && createPortal(children, mountNode)}
</iframe>
);
};
export default PortalIframe;

View File

@ -0,0 +1,17 @@
define([], function() {
return {
"PropertyPaneDescription": "Modern Content Editor Web Part with a twist: content can only be picked from approved locations.",
"BasicGroupName": "Web Part Properties",
"IsolatedMode": "Keep content isolated to prevent conflicts with other Web Parts.",
"DescriptionFieldLabel": "Title",
"LibraryPickerLabel": "Pick an approved library",
"LibraryItemPickerLabel": "Pick a file",
"Isolated": "Isolated Content",
"IframeWidth": "Width",
"IframeHeight": "Height",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams"
}
});

View File

@ -0,0 +1,20 @@
declare interface ICherryPickedContentWebPartStrings {
IsolatedMode: string;
IframeHeight: string;
IframeWidth: string;
Isolated: string;
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
LibraryPickerLabel: string;
LibraryItemPickerLabel: string;
AppLocalEnvironmentSharePoint: string;
AppLocalEnvironmentTeams: string;
AppSharePointEnvironment: string;
AppTeamsTabEnvironment: string;
}
declare module 'CherryPickedContentWebPartStrings' {
const strings: ICherryPickedContentWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -0,0 +1,29 @@
{
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -0,0 +1,39 @@
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
{
"name": "SPFx 1.15",
"image": "docker.io/m365pnp/spfx:latest",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
4321,
35729
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
// Not needed for SPFx>= 1.12.1
// "5432": {
// "protocol": "https",
// "label": "Workbench",
// "onAutoForward": "silent"
// },
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -0,0 +1,33 @@
echo
echo -e "\e[1;94mInstalling Node dependencies\e[0m"
npm install
## commands to create dev certificate and copy it to the root folder of the project
echo
echo -e "\e[1;94mGenerating dev certificate\e[0m"
gulp trust-dev-cert
# Convert the generated PEM certificate to a CER certificate
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
# Copy the PEM ecrtificate for non-Windows hosts
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
## add *.cer to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.cer' ./.gitignore
then
echo "# .CER Certificates" >> .gitignore
echo "*.cer" >> .gitignore
fi
## add *.pem to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.pem' ./.gitignore
then
echo "# .PEM Certificates" >> .gitignore
echo "*.pem" >> .gitignore
fi
echo
echo -e "\e[1;92mReady!\e[0m"
echo -e "\n\e[1;94m**********\nOptional: if you plan on using gulp serve, don't forget to add the container certificate to your local machine. Please visit https://aka.ms/spfx-devcontainer for more information\n**********"

View File

@ -0,0 +1,38 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
.heft
# 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
# .CER Certificates
*.cer
# .PEM Certificates
*.pem

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Hosted workbench",
"type": "pwa-chrome",
"request": "launch",
"url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222",
"-incognito"
]
}
]
}

View File

@ -0,0 +1,16 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": true,
"isCreatingSolution": true,
"version": "1.15.0-beta.1",
"libraryName": "react-ppp-pnpcontrols",
"libraryId": "04fdd42b-6e5b-4d68-a48c-c086c85a7887",
"environment": "spo",
"packageManager": "npm",
"solutionName": "react-ppp-pnpcontrols)",
"solutionShortDescription": "react-ppp-pnpcontrols) description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,101 @@
# Property Pane Portal PnP Controls
## Summary
The React-PPP-PnP-Controls sample showcases the use of the [Property Pane Portal](https://www.npmjs.com/package/property-pane-portal) to display the [PnP SPFx React controls](https://github.com/pnp/sp-dev-fx-controls-react) (version 3.7.0) in the SPFx Property Pane.
> We are NOT using the [SPFx Property Controls](https://github.com/pnp/sp-dev-fx-property-controls), that's the point of the sample.
![React-PPP-PnP-Controls-Sample](./assets/React-PPP-PnP-Controls-Sample.png)
## Compatibility
![SPFx 1.15](https://img.shields.io/badge/SPFx-1.15-green.svg)
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Solution
Solution|Author(s)
--------|---------
React-PPP-PnP-Controls | [Christophe Humbert](https://github.com/PathToSharePoint)
## Version history
Version|Date|Comments
-------|----|--------
0.1.0|March 20, 2022|
## Minimal Path to Awesome
* Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-ppp-pnp-controls) then unzip it)
* From your command line, change your current directory to the directory containing this sample (`react-ppp-pnp-controls`, located under `samples`)
* in the command line run:
* `npm install`
* `gulp serve`
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
## Features
This sample showcases the use of the **Property Pane Portal** NPM module. It allows us to use PnP SPFx React controls in the Property Pane, without the need to build custom property controls.
The Property Pane Portal module includes:
* The PropertyPaneHost function, which creates placeholders in the Property Pane
* The PropertyPanePortal component, which leverages React Portals to teleport React components to the Property Pane.
Implemented controls:
* Location Picker
* People Picker
* List Picker and List Item Picker (cascading selection)
## Known Issues
There are a couple minor issues with the Location Picker of the SPFx React Controls library. [I am working with the authors](https://github.com/pnp/sp-dev-fx-controls-react/issues/1125) to get them addressed in the next release. In the meantime, be aware that:
* the control will overflow its container width if the address is too long
* the control doesn't work on the root site
## References
* [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
* [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
* [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
* [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
* [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
## Help
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-ppp-pnp-controls%22) to see if anybody else is having the same issues.
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-ppp-pnp-controls) and see what the community is saying.
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-ppp-pnp-controls&template=bug-report.yml&sample=react-ppp-pnp-controls&authors=@PathToSharePoint&title=react-ppp-pnp-controls%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-ppp-pnp-controls&template=question.yml&sample=react-ppp-pnp-controls&authors=@PathToSharePoint&title=react-ppp-pnp-controls%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-ppp-pnp-controls&template=suggestion.yml&sample=react-ppp-pnp-controls&authors=@PathToSharePoint&title=react-ppp-pnp-controls%20-%20).
## 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.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-ppp-pnp-controls" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

View File

@ -0,0 +1,50 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-ppp-pnp-controls",
"source": "pnp",
"title": "Property Pane Portal PnP Controls",
"shortDescription": "The React-PPP-PnP-Controls sample showcases the use of the Property Pane Portal to display the PnP SPFx React controls in the SPFx Property Pane.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-ppp-pnp-controls",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-ppp-pnp-controls",
"longDescription": [
"The React-PPP-PnP-Controls sample showcases the use of the Property Pane Portal to display the PnP SPFx React controls in the SPFx Property Pane."
],
"creationDateTime": "2022-03-20",
"updateDateTime": "2022-03-20",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.14"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-ppp-pnp-controls/assets/React-PPP-PnP-Controls-Sample.png",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "PathToSharePoint",
"pictureUrl": "https://github.com/PathToSharePoint.png",
"name": "Christophe Humbert"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"hello-world-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/helloWorld/HelloWorldWebPart.js",
"manifest": "./src/webparts/helloWorld/HelloWorldWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"HelloWorldWebPartStrings": "lib/webparts/helloWorld/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-ppp-pnpcontrols-client-side-solution",
"id": "04fdd42b-6e5b-4d68-a48c-c086c85a7887",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "Christophe Humbert",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.15.0-beta.1"
},
"metadata": {
"shortDescription": {
"default": "react-ppp-pnpcontrols) description"
},
"longDescription": {
"default": "react-ppp-pnpcontrols) description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "react-ppp-pnpcontrols Feature",
"description": "The feature that activates elements of the react-ppp-pnpcontrols solution.",
"id": "539ffd20-c6d2-453c-b908-047d8a48d2fe",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/react-ppp-pnpcontrols.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

View File

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

View File

@ -0,0 +1,16 @@
'use strict';
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.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "react-ppp-pnpcontrols",
"version": "0.1.0",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.15.0-beta.1",
"@microsoft/sp-lodash-subset": "1.15.0-beta.1",
"@microsoft/sp-office-ui-fabric-core": "1.15.0-beta.1",
"@microsoft/sp-property-pane": "1.15.0-beta.1",
"@microsoft/sp-webpart-base": "1.15.0-beta.1",
"@pnp/spfx-controls-react": "3.7.0",
"office-ui-fabric-react": "7.181.1",
"property-pane-portal": "^0.2.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"tslib": "1.13.0"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.9": "^0.4.48",
"@microsoft/sp-build-web": "1.15.0-beta.1",
"@microsoft/sp-module-interfaces": "1.15.0-beta.1",
"@microsoft/sp-tslint-rules": "1.15.0-beta.1",
"@types/react": "16.9.51",
"@types/react-dom": "16.9.8",
"@types/webpack-env": "~1.15.2",
"ajv": "^6.12.5",
"gulp": "4.0.2"
}
}

View File

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

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "93a750f8-3094-40fd-90b2-89f7d0b8f628",
"alias": "HelloWorldWebPart",
"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,
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
"supportsThemeVariants": true,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Property Pane PnP Controls" },
"description": { "default": "Use of the Property Pane Portal (PPP) to insert PnP controls in the Property Pane." },
"officeFabricIconFontName": "PageHeaderEdit",
"properties": {
"description": "Property Pane Portal"
}
}]
}

View File

@ -0,0 +1,148 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneHorizontalRule,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import * as strings from 'HelloWorldWebPartStrings';
import HelloWorld from './components/HelloWorld';
import { IHelloWorldProps } from './components/IHelloWorldProps';
import { update } from '@microsoft/sp-lodash-subset';
import { PropertyPaneHost } from 'property-pane-portal';
import { CustomPropertyPane } from './components/CustomPropertyPane';
import { ILocationPickerItem } from "@pnp/spfx-controls-react/lib/LocationPicker";
export interface IHelloWorldWebPartProps {
description: string;
pnpListPicker: string;
pnpListItemPicker: any[];
pnpLocationPicker: ILocationPickerItem;
pnpPeoplePicker: any[];
}
export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
public render(): void {
const element: React.ReactElement<IHelloWorldProps> = React.createElement(
HelloWorld,
{
properties: this.properties,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName
}
);
const wpProps = {
properties: this.properties,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName
};
const customPropertyPaneProperties = {
context: this.context,
properties: this.properties,
updateWebPartProperty: this.updateWebPartProperty.bind(this),
};
ReactDom.render(
<>
{/* Web Part content */}
<HelloWorld {...wpProps} />
{/* Property Pane custom controls */}
<CustomPropertyPane {...customPropertyPaneProperties} />
</>,
this.domElement);
}
protected onInit(): Promise<void> {
this._environmentMessage = this._getEnvironmentMessage();
return super.onInit();
}
private _getEnvironmentMessage(): string {
if (!!this.context.sdks.microsoftTeams) { // running in Teams
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
}
return this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment;
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const {
semanticColors
} = currentTheme;
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText);
this.domElement.style.setProperty('--link', semanticColors.link);
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
private updateWebPartProperty(property, value) {
update(this.properties, property, () => value);
this.render();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
const hostProperties = {
context: this.context
};
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: "PnP Controls",
groupFields: [
PropertyPaneHost('pnpLocationPicker', hostProperties),
PropertyPaneHorizontalRule(),
PropertyPaneHost('pnpPeoplePicker', hostProperties),
PropertyPaneHorizontalRule(),
PropertyPaneHost('pnpListPicker', hostProperties),
PropertyPaneHost('pnpListItemPicker', hostProperties)
]
},
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,66 @@
import * as React from 'react';
import { ListPicker } from '@pnp/spfx-controls-react/lib/ListPicker';
import { ListItemPicker } from '@pnp/spfx-controls-react/lib/ListItemPicker';
import { PeoplePicker, PrincipalType } from "@pnp/spfx-controls-react/lib/PeoplePicker";
import { LocationPicker, ILocationPickerItem } from "@pnp/spfx-controls-react/lib/LocationPicker";
import { PropertyPanePortal } from 'property-pane-portal';
import { ICustomPropertyPaneProps } from './ICustomPropertyPaneProps';
export const CustomPropertyPane: React.FunctionComponent<ICustomPropertyPaneProps> = (props) => {
return (
<PropertyPanePortal context={props.context}>
<ListPicker
data-property="pnpListPicker"
context={props.context as any}
label="PnP List and Item Picker"
placeHolder="Select your list(s)"
selectedList={props.properties["pnpListPicker"]}
baseTemplate={100}
includeHidden={false}
multiSelect={false}
onSelectionChanged={(list: any) => {
props.updateWebPartProperty("pnpListPicker", list);
props.updateWebPartProperty("pnpListItemPicker", []);
}}
/>
{(props.properties["pnpListPicker"]) && (props.properties["pnpListPicker"].length == 36) &&
<ListItemPicker
data-property="pnpListItemPicker"
listId={props.properties["pnpListPicker"]}
defaultSelectedItems={props.properties["pnpListItemPicker"]}
columnInternalName='Title'
keyColumnInternalName='Id'
orderBy={"Id desc"}
itemLimit={2}
onSelectedItem={(item: any) => props.updateWebPartProperty("pnpListItemPicker", item)}
context={props.context as any}
/>
}
<LocationPicker
data-property="pnpLocationPicker"
context={props.context as any}
defaultValue={props.properties["pnpLocationPicker"]}
label="PnP Location"
onChange={(locValue: ILocationPickerItem) => props.updateWebPartProperty("pnpLocationPicker", locValue)}
/>
<PeoplePicker
data-property="pnpPeoplePicker"
context={props.context as any}
titleText="PnP People Picker"
personSelectionLimit={3}
defaultSelectedUsers={props.properties["pnpPeoplePicker"]?.map(user => user.secondaryText)}
showtooltip={true}
required={false}
disabled={false}
onChange={(items: any) => props.updateWebPartProperty("pnpPeoplePicker", items)}
showHiddenInUI={false}
principalTypes={[PrincipalType.User]}
resolveDelay={1000}
/>
</PropertyPanePortal>
);
};

View File

@ -0,0 +1,46 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.helloWorld {
overflow: hidden;
padding: 1em;
color: "[theme:bodyText, default: #323130]";
color: var(--bodyText);
&.teams {
font-family: $ms-font-family-fallbacks;
}
}
.welcome {
text-align: center;
}
.welcomeImage {
width: 100%;
max-width: 420px;
}
.links {
a {
text-decoration: none;
color: "[theme:link, default:#03787c]";
color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only
&:hover {
text-decoration: underline;
color: "[theme:linkHovered, default: #014446]";
color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only
}
}
}
.propertyTable {
border-collapse: collapse;
th {
padding: 10px;
}
td {
text-align: left;
border: 1px solid teal;
padding: 10px;
}
}

View File

@ -0,0 +1,67 @@
import * as React from 'react';
import styles from './HelloWorld.module.scss';
import { IHelloWorldProps } from './IHelloWorldProps';
import { escape } from '@microsoft/sp-lodash-subset';
export default class HelloWorld extends React.Component<IHelloWorldProps, {}> {
public render(): React.ReactElement<IHelloWorldProps> {
const {
properties,
isDarkTheme,
environmentMessage,
hasTeamsContext,
userDisplayName
} = this.props;
return (
<section className={`${styles.helloWorld} ${hasTeamsContext ? styles.teams : ''}`}>
<div className={styles.welcome}>
{properties["pnpLocationPicker"] ?
<iframe
width="500"
height="200"
frameBorder="0"
src={`https://www.bing.com/maps/embed?h=200&w=500&cp=${properties["pnpLocationPicker"].Coordinates.Latitude}~${properties["pnpLocationPicker"].Coordinates.Longitude}&lvl=11&typ=s&sty=r&src=SHELL&FORM=MBEDV8`}
scrolling="no"
>
</iframe>
:
<img alt="" src={isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} />
}
<h2>Well done, {escape(userDisplayName)}!</h2>
<div>{environmentMessage}</div>
</div>
<table className={styles.propertyTable}>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
{Object.keys(properties).map(key => {
return (
<tr key={key}>
<td>{key}</td>
<td><strong>{JSON.stringify(properties[key])}</strong></td>
</tr>
);
})}
</table>
<div>
<h3>Welcome to SharePoint Framework!</h3>
<p>
The SharePoint Framework (SPFx) is a extensibility model for Microsoft Viva, Microsoft Teams and SharePoint. It's the easiest way to extend Microsoft 365 with automatic Single Sign On, automatic hosting and industry standard tooling.
</p>
<h4>Learn more about SPFx development:</h4>
<ul className={styles.links}>
<li><a href="https://aka.ms/spfx" target="_blank">SharePoint Framework Overview</a></li>
<li><a href="https://aka.ms/spfx-yeoman-graph" target="_blank">Use Microsoft Graph in your solution</a></li>
<li><a href="https://aka.ms/spfx-yeoman-teams" target="_blank">Build for Microsoft Teams using SharePoint Framework</a></li>
<li><a href="https://aka.ms/spfx-yeoman-viva" target="_blank">Build for Microsoft Viva Connections using SharePoint Framework</a></li>
<li><a href="https://aka.ms/spfx-yeoman-store" target="_blank">Publish SharePoint Framework applications to the marketplace</a></li>
<li><a href="https://aka.ms/spfx-yeoman-api" target="_blank">SharePoint Framework API reference</a></li>
<li><a href="https://aka.ms/m365pnp" target="_blank">Microsoft 365 Developer Community</a></li>
</ul>
</div>
</section>
);
}
}

View File

@ -0,0 +1,8 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { IHelloWorldWebPartProps } from "../HelloWorldWebPart";
export interface ICustomPropertyPaneProps {
context: WebPartContext;
properties: IHelloWorldWebPartProps;
updateWebPartProperty: Function;
}

View File

@ -0,0 +1,15 @@
import { ILocationPickerItem } from "@pnp/spfx-controls-react/lib/LocationPicker";
export interface IHelloWorldProps {
properties: {
description: string;
pnpListPicker: string;
pnpListItemPicker: any[];
pnpLocationPicker: ILocationPickerItem;
pnpPeoplePicker: any[];
};
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
}

View File

@ -0,0 +1,11 @@
define([], function() {
return {
"PropertyPaneDescription": "Property Pane Portal for PnP Controls",
"BasicGroupName": "Standard Field",
"DescriptionFieldLabel": "Description Field",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams"
}
});

View File

@ -0,0 +1,14 @@
declare interface IHelloWorldWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
AppLocalEnvironmentSharePoint: string;
AppLocalEnvironmentTeams: string;
AppSharePointEnvironment: string;
AppTeamsTabEnvironment: string;
}
declare module 'HelloWorldWebPartStrings' {
const strings: IHelloWorldWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -0,0 +1,29 @@
{
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}