New sample SPFx webhooks real time (#351)

* first commit

* Implemented GetChanges API, Readme updated, New property "socket server url"

* added some scripts in order to automate the provisioning and deployment, readme updated, minor bug fix
This commit is contained in:
Giuliano De Luca 2017-11-06 08:32:25 +01:00 committed by Vesa Juvonen
parent 99ea9eefde
commit cbcc2041d4
32 changed files with 1132 additions and 0 deletions

View File

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

View File

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

View File

@ -0,0 +1,75 @@
// 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",
"json.schemas": [
{
"fileMatch": [
"/config/config.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configJson/schemas/config-v1.schema.json"
},
{
"fileMatch": [
"/config/copy-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyAssets/copy-assets.schema.json"
},
{
"fileMatch": [
"/config/deploy-azure-storage.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/deployAzureStorage/deploy-azure-storage.schema.json"
},
{
"fileMatch": [
"/config/package-solution.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/packageSolution/package-solution.schema.json"
},
{
"fileMatch": [
"/config/serve.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-serve/lib/serve.schema.json"
},
{
"fileMatch": [
"/config/tslint.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-typescript/lib/schemas/tslint.schema.json"
},
{
"fileMatch": [
"/config/write-manifests.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/writeManifests/write-manifests.schema.json"
},
{
"fileMatch": [
"/config/configure-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack.schema.json"
},
{
"fileMatch": [
"/config/configure-external-bundling-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack-external-bundling.schema.json"
},
{
"fileMatch": [
"/copy-static-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyStaticAssets/copy-static-assets.schema.json"
}
]
}

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.3.2",
"libraryName": "react-webhooks-realtime",
"libraryId": "d4eee588-a8e6-45ce-b9e1-c40f0b92ef2d",
"environment": "spo"
}
}

View File

@ -0,0 +1,118 @@
#################
# Configuration #
#################
$catalogSite = "https://giuleon.sharepoint.com/sites/apps" # => App Catalog site
$catalogRelativePath = "sites/apps/AppCatalog" # => App Catalog relative url
#######
# End #
#######
# Get Web Request
function GetRequest ($apiUrl, $webSession) {
return Invoke-WebRequest -Uri $apiUrl -Method Get -WebSession $webSession
}
# Post Web Request
function PostRequest($apiUrl, $webSession, $body) {
return Invoke-WebRequest -Uri $apiUrl -Body $body -Method Post -WebSession $webSession
}
# Settting the right parameters value
function setXmlMapping($xmlBody, $siteId, $webId, $listId, $fileId, $fileVersion, $skipDeployment) {
# Replace the random token with a random guid
$randomGuid = [guid]::NewGuid()
if($skipDeployment -eq $True){
$skipDeployment = "true"
}
else{
$skipDeployment = "false"
}
$xmlBody = [regex]::replace($xmlBody, "{randomId}", $randomGuid)
# Replace the site ID token with the actual site ID string
$xmlBody = [regex]::replace($xmlBody, "{siteId}", $siteId)
# Replace the web ID token with the actual web ID string
$xmlBody = [regex]::replace($xmlBody, "{webId}", $webId)
# Replace the list ID token with the actual list ID string
$xmlBody = [regex]::replace($xmlBody, "{listId}", $listId)
# Replace the item ID token with the actual item ID number
$xmlBody = [regex]::replace($xmlBody, "{itemId}", $fileId)
# Replace the file version token with the actual file version number
$xmlBody = [regex]::replace($xmlBody, "{fileVersion}", $fileVersion)
# Replace the skipFeatureDeployment token with the skipFeatureDeployment option
$xmlBody = [regex]::replace($xmlBody, "{skipFeatureDeployment}", $skipDeployment)
return $xmlBody;
}
Write-Host ***************************************** -ForegroundColor Yellow
Write-Host * Uploading the sppkg on the AppCatalog * -ForegroundColor Yellow
Write-Host ***************************************** -ForegroundColor Yellow
$packageConfig = Get-Content -Raw -Path .\config\package-solution.json | ConvertFrom-Json
$packagePath = Join-Path "sharepoint/" $packageConfig.paths.zippedPackage -Resolve
$skipFeatureDeployment = $packageConfig.solution.skipFeatureDeployment
# Connect-PnPOnline $catalogSite -Credentials (Get-Credential)
Connect-PnPOnline $catalogSite -Credentials giuleon
Add-PnPFile -Path $packagePath -Folder "AppCatalog"
Write-Host *************************************************** -ForegroundColor Yellow
Write-Host * The SPFx solution has been succesfully uploaded to the AppCatalog * -ForegroundColor Yellow
Write-Host *************************************************** -ForegroundColor Yellow
# Connect to SharePoint Online
$targetSite = "https://giuleon.sharepoint.com/sites/apps"
$targetSiteUri = [System.Uri]$targetSite
# Retrieve the client credentials and the related Authentication Cookies
$context = (Get-PnPWeb).Context
$credentials = $context.Credentials
$authenticationCookies = $credentials.GetAuthenticationCookie($targetSiteUri, $true)
# Set the Authentication Cookies and the Accept HTTP Header
$webSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$webSession.Cookies.SetCookies($targetSiteUri, $authenticationCookies)
$webSession.Headers.Add("Accept", "application/json;odata=verbose")
$apiUrl = $catalogSite + "/_api/contextinfo?$"+"select=FormDigestValue"
$result = PostRequest -apiUrl $apiUrl -webSession $webSession
$formDigest = $result.Content | ConvertFrom-Json
Write-Host "FormDigestValue - " $formDigest.d.GetContextWebInformation.FormDigestValue
$formDigest = $formDigest.d.GetContextWebInformation.FormDigestValue
$webSession.Headers.Add("X-RequestDigest", $formDigest)
# Set request variables
$apiUrl = "$targetSite" + "/_api/site?$"+"select=Id"
# Make the REST request
$webRequest = GetRequest -apiUrl $apiUrl -webSession $webSession # Invoke-WebRequest -Uri $apiUrl -Method Get -WebSession $webSession
$response = $webRequest.Content | ConvertFrom-Json
$siteId = $response.d.Id
Write-Host "Site Id - " $response.d.Id
# Retrieving webId and listId
$apiUrl = "$targetSite" + "/_api/web/getList('$catalogRelativePath')?$"+"select=Id,ParentWeb/Id`&`$"+"expand=ParentWeb"
$webRequest = GetRequest -apiUrl $apiUrl -webSession $webSession # Invoke-WebRequest -Uri $apiUrl -Method Get -WebSession $webSession
$response = $webRequest.Content | ConvertFrom-Json
$webId = $response.d.ParentWeb.Id
$listId = $response.d.Id
Write-Host "Web Id - " $webId " / List Id - " + $listId
# Get the ListItemAllFields Id and Version
$fileName = $packageConfig.paths.zippedPackage.Substring($packageConfig.paths.zippedPackage.LastIndexOf("/")+1)
$apiUrl = "$targetSite" + "/_api/web/GetFolderByServerRelativeUrl('AppCatalog')/Files('$fileName')?$"+"expand=ListItemAllFields`&`$" + "select=ListItemAllFields/ID,ListItemAllFields/owshiddenversion"
$webRequest = GetRequest -apiUrl $apiUrl -webSession $webSession
$response = $webRequest.Content -replace '"id":', '"id_":' | ConvertFrom-Json
$fileId = $response.d.ListItemAllFields.id_
$fileVersion = $response.d.ListItemAllFields.owshiddenversion
Write-Host "ListItem Id - " $fileId " / Version - " $fileVersion
# Read the xml
$xmlBody = Get-Content DeploySPFxToAppCatalogRequestBody.xml -Encoding UTF8
$xmlBody = setXmlMapping -xmlBody $xmlBody -siteId $siteId -webId $webId -listId $listId -fileId $fileId -fileVersion $fileVersion -skipDeployment $skipFeatureDeployment
Write-Host "deployment in progress....."
# Deploy the sspkg
$webSession.Headers.Add("Content-type", "application/xml")
$apiUrl = $catalogSite + "/_vti_bin/client.svc/ProcessQuery"
$result = PostRequest -apiUrl $apiUrl -webSession $webSession -body $xmlBody
Write-Host $result

View File

@ -0,0 +1,148 @@
<Request
xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library">
<Actions>
<Method Name="SetFieldValue" Id="18" ObjectPathId="10" Version="{fileVersion}">
<Parameters>
<Parameter Type="String">IsClientSideSolutionDeployed</Parameter>
<Parameter Type="Boolean">true</Parameter>
</Parameters>
</Method>
<Method Name="SetFieldValue" Id="19" ObjectPathId="10" Version="{fileVersion}">
<Parameters>
<Parameter Type="String">IsClientSideSolutionCurrentVersionDeployed</Parameter>
<Parameter Type="Boolean">true</Parameter>
</Parameters>
</Method>
<Method Name="SetFieldValue" Id="20" ObjectPathId="10" Version="{fileVersion}">
<Parameters>
<Parameter Type="String">SkipFeatureDeployment</Parameter>
<Parameter Type="Boolean">{skipFeatureDeployment}</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="21" ObjectPathId="10" Version="{fileVersion}" />
<Query Id="22" ObjectPathId="10">
<Query SelectAllProperties="false">
<Properties>
<Property Name="FileSystemObjectType" ScalarProperty="true" />
<Property Name="Id" ScalarProperty="true" />
<Property Name="ServerRedirectedEmbedUri" ScalarProperty="true" />
<Property Name="ServerRedirectedEmbedUrl" ScalarProperty="true" />
<Property Name="ID" ScalarProperty="true" />
<Property Name="ContentTypeId" ScalarProperty="true" />
<Property Name="Created" ScalarProperty="true" />
<Property Name="Author" ScalarProperty="true" />
<Property Name="Modified" ScalarProperty="true" />
<Property Name="Editor" ScalarProperty="true" />
<Property Name="_HasCopyDestinations" ScalarProperty="true" />
<Property Name="_CopySource" ScalarProperty="true" />
<Property Name="_ModerationStatus" ScalarProperty="true" />
<Property Name="_ModerationComments" ScalarProperty="true" />
<Property Name="FileRef" ScalarProperty="true" />
<Property Name="FileDirRef" ScalarProperty="true" />
<Property Name="Last_x0020_Modified" ScalarProperty="true" />
<Property Name="Created_x0020_Date" ScalarProperty="true" />
<Property Name="File_x0020_Size" ScalarProperty="true" />
<Property Name="FSObjType" ScalarProperty="true" />
<Property Name="SortBehavior" ScalarProperty="true" />
<Property Name="CheckedOutUserId" ScalarProperty="true" />
<Property Name="IsCheckedoutToLocal" ScalarProperty="true" />
<Property Name="CheckoutUser" ScalarProperty="true" />
<Property Name="FileLeafRef" ScalarProperty="true" />
<Property Name="UniqueId" ScalarProperty="true" />
<Property Name="SyncClientId" ScalarProperty="true" />
<Property Name="ProgId" ScalarProperty="true" />
<Property Name="ScopeId" ScalarProperty="true" />
<Property Name="VirusStatus" ScalarProperty="true" />
<Property Name="CheckedOutTitle" ScalarProperty="true" />
<Property Name="_CheckinComment" ScalarProperty="true" />
<Property Name="Modified_x0020_By" ScalarProperty="true" />
<Property Name="Created_x0020_By" ScalarProperty="true" />
<Property Name="File_x0020_Type" ScalarProperty="true" />
<Property Name="HTML_x0020_File_x0020_Type" ScalarProperty="true" />
<Property Name="_SourceUrl" ScalarProperty="true" />
<Property Name="_SharedFileIndex" ScalarProperty="true" />
<Property Name="MetaInfo" ScalarProperty="true" />
<Property Name="_Level" ScalarProperty="true" />
<Property Name="_IsCurrentVersion" ScalarProperty="true" />
<Property Name="ItemChildCount" ScalarProperty="true" />
<Property Name="FolderChildCount" ScalarProperty="true" />
<Property Name="Restricted" ScalarProperty="true" />
<Property Name="OriginatorId" ScalarProperty="true" />
<Property Name="NoExecute" ScalarProperty="true" />
<Property Name="ContentVersion" ScalarProperty="true" />
<Property Name="_ComplianceFlags" ScalarProperty="true" />
<Property Name="_ComplianceTag" ScalarProperty="true" />
<Property Name="_ComplianceTagWrittenTime" ScalarProperty="true" />
<Property Name="_ComplianceTagUserId" ScalarProperty="true" />
<Property Name="BSN" ScalarProperty="true" />
<Property Name="_ListSchemaVersion" ScalarProperty="true" />
<Property Name="_Dirty" ScalarProperty="true" />
<Property Name="_Parsable" ScalarProperty="true" />
<Property Name="AccessPolicy" ScalarProperty="true" />
<Property Name="_VirusStatus" ScalarProperty="true" />
<Property Name="_VirusVendorID" ScalarProperty="true" />
<Property Name="_VirusInfo" ScalarProperty="true" />
<Property Name="AppAuthor" ScalarProperty="true" />
<Property Name="AppEditor" ScalarProperty="true" />
<Property Name="SMTotalSize" ScalarProperty="true" />
<Property Name="SMLastModifiedDate" ScalarProperty="true" />
<Property Name="SMTotalFileStreamSize" ScalarProperty="true" />
<Property Name="SMTotalFileCount" ScalarProperty="true" />
<Property Name="owshiddenversion" ScalarProperty="true" />
<Property Name="_UIVersion" ScalarProperty="true" />
<Property Name="_UIVersionString" ScalarProperty="true" />
<Property Name="InstanceID" ScalarProperty="true" />
<Property Name="Order" ScalarProperty="true" />
<Property Name="GUID" ScalarProperty="true" />
<Property Name="WorkflowVersion" ScalarProperty="true" />
<Property Name="WorkflowInstanceID" ScalarProperty="true" />
<Property Name="ParentVersionString" ScalarProperty="true" />
<Property Name="ParentLeafName" ScalarProperty="true" />
<Property Name="DocConcurrencyNumber" ScalarProperty="true" />
<Property Name="ParentUniqueId" ScalarProperty="true" />
<Property Name="StreamHash" ScalarProperty="true" />
<Property Name="ComplianceAssetId" ScalarProperty="true" />
<Property Name="Title" ScalarProperty="true" />
<Property Name="AppProductID" ScalarProperty="true" />
<Property Name="AssetID" ScalarProperty="true" />
<Property Name="AppPublisher" ScalarProperty="true" />
<Property Name="AppThumbnailURL" ScalarProperty="true" />
<Property Name="AppDescription" ScalarProperty="true" />
<Property Name="AppShortDescription" ScalarProperty="true" />
<Property Name="AppVersion" ScalarProperty="true" />
<Property Name="AppSupportURL" ScalarProperty="true" />
<Property Name="AppVideoURL" ScalarProperty="true" />
<Property Name="IsAppPackageEnabled" ScalarProperty="true" />
<Property Name="SharePointAppCategory" ScalarProperty="true" />
<Property Name="AppPrerequisitesXML" ScalarProperty="true" />
<Property Name="IsFeaturedApp" ScalarProperty="true" />
<Property Name="AppMetadataLocale" ScalarProperty="true" />
<Property Name="IsDefaultAppMetadataLocale" ScalarProperty="true" />
<Property Name="AppImageURL1" ScalarProperty="true" />
<Property Name="AppImageURL2" ScalarProperty="true" />
<Property Name="AppImageURL3" ScalarProperty="true" />
<Property Name="AppImageURL4" ScalarProperty="true" />
<Property Name="AppImageURL5" ScalarProperty="true" />
<Property Name="AppPackageHash" ScalarProperty="true" />
<Property Name="IsValidAppPackage" ScalarProperty="true" />
<Property Name="AppPackageErrorMessage" ScalarProperty="true" />
<Property Name="AppPermissionXML" ScalarProperty="true" />
<Property Name="AppSubtypeID" ScalarProperty="true" />
<Property Name="IsAutoHostedApp" ScalarProperty="true" />
<Property Name="AppTitleInfo" ScalarProperty="true" />
<Property Name="AppSupportedLocales" ScalarProperty="true" />
<Property Name="IsClientSideSolution" ScalarProperty="true" />
<Property Name="IsClientSideSolutionDeployed" ScalarProperty="true" />
<Property Name="ExternalContentDomains" ScalarProperty="true" />
<Property Name="IsClientSideSolutionCurrentVersionDeployed" ScalarProperty="true" />
<Property Name="SkipFeatureDeployment" ScalarProperty="true" />
<Property Name="PackageDefaultSkipFeatureDeployment" ScalarProperty="true" />
<Property Name="UniqueSolutionId" ScalarProperty="true" />
</Properties>
</Query>
</Query>
</Actions>
<ObjectPaths>
<Identity Id="10" Name="{randomId}|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:{siteId}:web:{webId}:list:{listId}:item:{itemId},1" />
</ObjectPaths>
</Request>

View File

@ -0,0 +1,24 @@
#################
# Configuration #
#################
$cdnSite = "https://giuleon.sharepoint.com/" # => CDN SharePoint site
$cdnLib = "cdn/SPFx-react-webhooks-realtime" # => Document library and eventual folders
#######
# End #
#######
Write-Host ************************************************************************************** -ForegroundColor Yellow
Write-Host * Reading the cdnBasePath from write-manifests.json and collectiong the bundle files * -ForegroundColor Yellow
Write-Host ************************************************************************************** -ForegroundColor Yellow
$cdnConfig = Get-Content -Raw -Path .\config\copy-assets.json | ConvertFrom-Json
$bundlePath = Convert-Path $cdnConfig.deployCdnPath
$files = Get-ChildItem $bundlePath\*.*
Write-Host **************************************** -ForegroundColor Yellow
Write-Host Uploading the bundle on Office 365 CDN * -ForegroundColor Yellow
Write-Host **************************************** -ForegroundColor Yellow
Connect-PnPOnline $cdnSite -Credentials giuleon
foreach ($file in $files) {
$fullPath = $file.DirectoryName + "\" + $file.Name
Add-PnPFile -Path $fullPath -Folder $cdnLib
}

View File

@ -0,0 +1,9 @@
$spSite = "https://giuleon.sharepoint.com/sites/demo" # => SharePoint site
$spListTitle = "Events" # => List name
Connect-PnPOnline $spSite -Credentials (Get-Credential)
New-PnPList -Title $spListTitle -Template GenericList
Add-PnPField -List $spListTitle -DisplayName "Description" -InternalName "SPFxDescription" -Type Text -Group "SPFx Group" -AddToDefaultView
Add-PnPField -List $spListTitle -DisplayName "Thumbnail" -InternalName "SPFxThumbnail" -Type Url -Group "SPFx Group" -AddToDefaultView

View File

@ -0,0 +1,58 @@
# Webhooks Realtime
## Summary
This web part demonstrates how to leverage the capabilities of SharePoint Webhooks.
The libraries used by this web part are Socket.io, sp pnp js, moment.
![Preview](./assets/spfx-react-webhooks-realtime.gif)
### Solution architecture
![Architecture](./assets/Architecture.png)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
> In order to use properly this web part is necessary follow these steps:
> * Istall a webserver that will receive the webhooks, for this PoC I created a NodeJs Application hosted on Azure take a look on my solution [https://github.com/giuleon/SharePoint-Webhooks-Broadcaster](https://github.com/giuleon/SharePoint-Webhooks-Broadcaster)
> * run the Powershell script **ProvisioningArtifacts.ps1** in order to provision the list Events which is required for this web part
> * Create a new webhooks subscription for the SharePoint List **Events** (that will be installed by running the script **ProvisioningArtifacts.ps1**), as you prefer, across your solution or Postman, please read the following guideline to achieve this goal [https://docs.microsoft.com/en-us/sharepoint/dev/apis/webhooks/overview-sharepoint-webhooks](https://docs.microsoft.com/en-us/sharepoint/dev/apis/webhooks/overview-sharepoint-webhooks)
> * The web part has been developed (GetChanges API) to notify new items added in the **Events** list
## Solution
Solution|Author(s)
--------|---------
react-webhooks-realtime|Giuliano De Luca ([@giuleon](https://twitter.com/giuleon) , [www.delucagiuliano.com](delucagiuliano.com))
## Version history
Version|Date|Comments
-------|----|--------
1.0|October 29, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- How to leverage the capabilities of SharePoint webhooks.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-webhooks-realtime" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

View File

@ -0,0 +1,20 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"real-time-list-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/realTimeList/RealTimeListWebPart.js",
"manifest": "./src/webparts/realTimeList/RealTimeListWebPart.manifest.json"
}
]
}
},
"externals": {
"sp-pnp-js": "https://cdnjs.cloudflare.com/ajax/libs/sp-pnp-js/3.0.1/pnp.min.js"
},
"localizedResources": {
"RealTimeListWebPartStrings": "lib/webparts/realTimeList/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-webhooks-realtime-client-side-solution",
"id": "d4eee588-a8e6-45ce-b9e1-c40f0b92ef2d",
"version": "1.0.0.0",
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-webhooks-realtime.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

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

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "https://publiccdn.sharepointonline.com/giuleon.sharepoint.com/cdn/SPFx-react-webhooks-realtime"
}

View File

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

View File

@ -0,0 +1,38 @@
{
"name": "react-webhooks-realtime",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.3.0",
"@microsoft/sp-lodash-subset": "~1.3.0",
"@microsoft/sp-webpart-base": "~1.3.0",
"@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"@uifabric/example-app-base": "^5.1.2",
"moment": "^2.19.1",
"react": "15.4.2",
"react-dom": "15.4.2",
"socket.io-client": "^2.0.4",
"sp-pnp-js": "^3.0.1"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.3.0",
"@microsoft/sp-module-interfaces": "~1.3.0",
"@microsoft/sp-webpart-workbench": "~1.3.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "874bb168-abe1-4176-92c2-24251077c23e",
"alias": "RealTimeListWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "Real Time List" },
"description": { "default": "get the last changes in real time" },
"officeFabricIconFontName": "News",
"properties": {
"description": "RealTimeList"
}
}]
}

View File

@ -0,0 +1,72 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'RealTimeListWebPartStrings';
import RealTimeList from './components/RealTimeList';
import { IRealTimeListProps } from './components/IRealTimeListProps';
import pnp from "sp-pnp-js";
export interface IRealTimeListWebPartProps {
socketserverurl: string;
siteUrl: string;
}
export default class RealTimeListWebPart extends BaseClientSideWebPart<IRealTimeListWebPartProps> {
// Ovverriding onInit in order to set up the sp pnp js with the web part context
public onInit(): Promise<void> {
return super.onInit().then(_ => {
// establish SPFx context
pnp.setup({
spfxContext: this.context
});
});
}
public render(): void {
const element: React.ReactElement<IRealTimeListProps > = React.createElement(
RealTimeList,
{
socketserverurl: this.properties.socketserverurl,
siteUrl: this.context.pageContext.web.absoluteUrl
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('socketserverurl', {
label: strings.SocketserverurlFieldLabel
})
]
}
]
}
]
};
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
}

View File

@ -0,0 +1,4 @@
export interface IRealTimeListProps {
socketserverurl: string;
siteUrl: string;
}

View File

@ -0,0 +1,11 @@
import {
IColumn
} from 'office-ui-fabric-react/lib/DetailsList';
export interface IRealTimeListState {
sortedItems?: any[];
columns?: IColumn[];
loading?: boolean;
newsFeed?: string;
newsFeedVisible?: boolean;
}

View File

@ -0,0 +1,52 @@
.realTimeList {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
padding: 0;//20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: "[theme:themePrimary, default:#0078d7]";
border-color: "[theme:themePrimary, default:#0078d7]";
color: #ffffff;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,270 @@
import * as React from 'react';
import styles from './RealTimeList.module.scss';
import { IRealTimeListProps } from './IRealTimeListProps';
import { IRealTimeListState } from './IRealTimeListState';
import { escape } from '@microsoft/sp-lodash-subset';
import pnp from "sp-pnp-js";
import { Web } from "sp-pnp-js";
import * as io from 'socket.io-client';
import { createListItems } from '@uifabric/example-app-base';
import { autobind } from 'office-ui-fabric-react/lib/Utilities';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { DefaultButton, IButtonProps } from 'office-ui-fabric-react/lib/Button';
import { Label } from 'office-ui-fabric-react/lib/Label';
import {
DetailsList,
buildColumns,
IColumn
} from 'office-ui-fabric-react/lib/DetailsList';
import {
Spinner,
SpinnerSize
} from 'office-ui-fabric-react/lib/Spinner';
import * as moment from 'moment';
let _items: any[];
let _lastQueryDate: moment.Moment;
export interface IList {
Id: number;
Title: string;
SPFxDescription: string;
SPFxThumbnail: ITumbnailUrl;
}
export interface ITumbnailUrl {
Url: string;
}
export default class RealTimeList extends React.Component<IRealTimeListProps, IRealTimeListState> {
public componentDidMount(): void {
if (this.props.socketserverurl != null && this.props.socketserverurl != "" && this.props.socketserverurl !== undefined) {
this._connectSocket(this.props.socketserverurl);
}
}
public componentWillReceiveProps(nextProps: IRealTimeListProps): void {
if (nextProps.socketserverurl != null && nextProps.socketserverurl != "" && nextProps.socketserverurl !== undefined) {
this._connectSocket(nextProps.socketserverurl);
}
}
constructor(props: IRealTimeListProps, state: IRealTimeListState) {
super(props);
_items = [];
this.state = {
sortedItems: _items,
columns: _buildColumns(),
loading: true
};
}
public render(): React.ReactElement<IRealTimeListProps> {
if (this.props.siteUrl.toLowerCase().indexOf("wwww.contoso.com") >= 0
|| this.props.socketserverurl === undefined || this.props.socketserverurl === "") {
return (
<div className={styles.realTimeList}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
<span className="ms-font-xl ms-fontColor-white">Connect the web part with SharePoint and configuring it before to begin</span>
</div>
</div>
</div>
</div>
);
}
let { sortedItems, columns } = this.state;
const loading: JSX.Element = (this.state.loading == true) ? <Spinner size={SpinnerSize.large} /> : null;
const list: JSX.Element = (this.state.loading == false) ?
<DetailsList
items={sortedItems as any[]}
setKey='set'
columns={columns}
onRenderItemColumn={_renderItemColumn}
onColumnHeaderClick={this._onColumnClick}
onItemInvoked={this._onItemInvoked}
onColumnHeaderContextMenu={this._onColumnHeaderContextMenu}
/>
: null;
const newsFeed: JSX.Element =
this.state.newsFeedVisible == true ?
<DefaultButton
text={this.state.newsFeed}
onClick={() => this._loadList()}
/>
: null;
return (
<div className={styles.realTimeList}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-white ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg6 ms-xl6 ms-xlPush5 ms-lgPush5">
{newsFeed}
</div>
</div>
<div className={`ms-Grid-row ms-bgColor-white ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg12 ms-xl12">
{loading}
</div>
</div>
<div className={`ms-Grid-row ms-bgColor-white ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg12 ms-xl12">
{list}
</div>
</div>
</div>
</div>
);
}
private toTicks(date: moment.Moment): number {
return (date.valueOf() * 10000) + 621355968000000000;
}
private async _connectSocket(socketServerUrl: string) {
// Connect to the server
const socket = io(socketServerUrl);
// Add the socket io listeners
socket.on('list:changes', (data) => {
this._getListChanges(data);
console.log(JSON.stringify(data));
});
await this._loadList();
}
private async _getListChanges(dataWebhooks: any): Promise<void> {
let dataParsed = JSON.parse(dataWebhooks);
let resource = dataParsed[0].resource;
let changeToken = `1;3;${resource};${this.toTicks(_lastQueryDate)};-1`;
let changes = await pnp.sp.web.lists.getByTitle("Events").getChanges(
{
Add: true,
Item: true,
ChangeTokenStart: { StringValue: changeToken }
});
console.log(changes);
console.log(_lastQueryDate);
console.log(_items.length);
if (changes.length > 0) {
let newsFeedText = (changes.length == 1) ? changes.length + " new item" : changes.length + " new items";
this.setState({
newsFeedVisible: true,
newsFeed: newsFeedText
});
}
}
private async _loadList(): Promise<void> {
this.setState({
loading: true
});
let items = await pnp.sp.web.lists.getByTitle("Events").items.select("Id", "Title", "SPFxDescription", "SPFxThumbnail")
.orderBy("Modified", false).get();
_items = items.map((item: IList, index: number) => {
return {
thumbnail: item.SPFxThumbnail != null ? item.SPFxThumbnail.Url : "",
key: item.Id,
name: item.Title,
description: item.SPFxDescription
}
});
this.setState({
sortedItems: _items,
columns: _buildColumns(),
loading: false,
newsFeedVisible: false
});
_lastQueryDate = moment();
}
private _getListItems(count: number, startIndex: number = 0): any {
// get all the items from a list
pnp.sp.web.lists.getByTitle("Events").items.get().then((items: any[]) => {
console.log(items);
});
}
@autobind
private _onColumnClick(event: React.MouseEvent<HTMLElement>, column: IColumn) {
let { sortedItems, columns } = this.state;
let isSortedDescending = column.isSortedDescending;
// If we've sorted this column, flip it.
if (column.isSorted) {
isSortedDescending = !isSortedDescending;
}
// Sort the items.
sortedItems = sortedItems!.concat([]).sort((a, b) => {
let firstValue = a[column.fieldName];
let secondValue = b[column.fieldName];
if (isSortedDescending) {
return firstValue > secondValue ? -1 : 1;
} else {
return firstValue > secondValue ? 1 : -1;
}
});
// Reset the items and columns to match the state.
this.setState({
sortedItems: sortedItems,
columns: columns!.map(col => {
col.isSorted = (col.key === column.key);
if (col.isSorted) {
col.isSortedDescending = isSortedDescending;
}
return col;
})
});
}
private _onColumnHeaderContextMenu(column: IColumn | undefined, ev: React.MouseEvent<HTMLElement> | undefined): void {
console.log(`column ${column!.key} contextmenu opened.`);
}
private _onItemInvoked(item: any, index: number | undefined): void {
alert(`Item ${item.name} at index ${index} has been invoked.`);
}
}
function _buildColumns() {
if (_items.length == 0) {
return [];
}
let columns = buildColumns(_items);
let thumbnailColumn = columns.filter(column => column.name === 'thumbnail')[0];
// Special case one column's definition.
thumbnailColumn.name = '';
thumbnailColumn.maxWidth = 100;
let keyColumn = columns.filter(column => column.name === 'key')[0];
keyColumn.maxWidth = 100;
let nameColumn = columns.filter(column => column.name === 'name')[0];
nameColumn.maxWidth = 200;
let descriptionColumn = columns.filter(column => column.name === 'description')[0];
descriptionColumn.maxWidth = 300;
return columns;
}
function _renderItemColumn(item: any, index: number, column: IColumn) {
let fieldContent = item[column.fieldName];
switch (column.key) {
case 'thumbnail':
return <Image src={fieldContent} width={50} height={50} imageFit={ImageFit.cover} />;
case 'name':
return <Link href='#'>{fieldContent}</Link>;
case 'color':
return <span data-selection-disabled={true} style={{ color: fieldContent }}>{fieldContent}</span>;
default:
return <span>{fieldContent}</span>;
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Configuration Panel",
"SocketserverurlFieldLabel": "Insert the Socket.IO server URL"
}
});

View File

@ -0,0 +1,10 @@
declare interface IRealTimeListWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
SocketserverurlFieldLabel: string;
}
declare module 'RealTimeListWebPartStrings' {
const strings: IRealTimeListWebPartStrings;
export = strings;
}

View File

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

View File

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

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />