Merge pull request #2411 from PathToSharePoint/main
New sample React-Cherry-Picked-Content
|
@ -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"
|
||||
}
|
|
@ -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**********"
|
|
@ -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
|
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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" />
|
After Width: | Height: | Size: 1.1 MiB |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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'));
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Chart.js Bubble Chart</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,39 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Chartist Animated Lines</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Agenda Custom View - Microsoft Graph Toolkit</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,70 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Emails Custom View - Microsoft Graph Toolkit</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,25 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>People Components - Microsoft Graph Toolkit</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,23 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>My Teams - MS Graph Client</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -0,0 +1,16 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Site Lists - SharePoint REST API</h3>
|
||||
<p>Isolated mode: ⚠ 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>
|
|
@ -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>
|
|
@ -0,0 +1,92 @@
|
|||
<div style="background-color: CadetBlue; padding: 20px;">
|
||||
<h3>Site Lists - pocketSOAP</h3>
|
||||
<p>Isolated mode: ⚠ 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(/"/g, '"')
|
||||
.replace(/'/g, '\'')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/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(/<xsd:schema>[\s\S]+?<\/xsd:schema>/g, "");
|
||||
spWS.soapEnvelope = spWS.soapEnvelope.replace(/<([\S]+)>[^;]+<\/\1>/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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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’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 “Property Pane Portal”), weeks to test it, and then months to start writing about it 😊. In the next episodes, I’ll share some samples of the PPP in action, and I’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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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"
|
||||
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -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"
|
||||
}
|
||||
];
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
17
samples/react-cherry-picked-content/src/webparts/cherryPickedContent/loc/en-us.js
vendored
Normal 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"
|
||||
}
|
||||
});
|
20
samples/react-cherry-picked-content/src/webparts/cherryPickedContent/loc/mystrings.d.ts
vendored
Normal 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;
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 542 B |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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**********"
|
|
@ -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
|
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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" />
|
After Width: | Height: | Size: 748 KiB |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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'));
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { IHelloWorldWebPartProps } from "../HelloWorldWebPart";
|
||||
|
||||
export interface ICustomPropertyPaneProps {
|
||||
context: WebPartContext;
|
||||
properties: IHelloWorldWebPartProps;
|
||||
updateWebPartProperty: Function;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 542 B |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|