diff --git a/samples/react-webhooks-realtime/.editorconfig b/samples/react-webhooks-realtime/.editorconfig new file mode 100644 index 000000000..8ffcdc4ec --- /dev/null +++ b/samples/react-webhooks-realtime/.editorconfig @@ -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 \ No newline at end of file diff --git a/samples/react-webhooks-realtime/.gitignore b/samples/react-webhooks-realtime/.gitignore new file mode 100644 index 000000000..b19bbe123 --- /dev/null +++ b/samples/react-webhooks-realtime/.gitignore @@ -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 diff --git a/samples/react-webhooks-realtime/.vscode/settings.json b/samples/react-webhooks-realtime/.vscode/settings.json new file mode 100644 index 000000000..975da89f4 --- /dev/null +++ b/samples/react-webhooks-realtime/.vscode/settings.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/.yo-rc.json b/samples/react-webhooks-realtime/.yo-rc.json new file mode 100644 index 000000000..17f2a76fa --- /dev/null +++ b/samples/react-webhooks-realtime/.yo-rc.json @@ -0,0 +1,8 @@ +{ + "@microsoft/generator-sharepoint": { + "version": "1.3.2", + "libraryName": "react-webhooks-realtime", + "libraryId": "d4eee588-a8e6-45ce-b9e1-c40f0b92ef2d", + "environment": "spo" + } +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/DeploySPFxToAppCatalog.ps1 b/samples/react-webhooks-realtime/DeploySPFxToAppCatalog.ps1 new file mode 100644 index 000000000..a084b9238 --- /dev/null +++ b/samples/react-webhooks-realtime/DeploySPFxToAppCatalog.ps1 @@ -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 \ No newline at end of file diff --git a/samples/react-webhooks-realtime/DeploySPFxToAppCatalogRequestBody.xml b/samples/react-webhooks-realtime/DeploySPFxToAppCatalogRequestBody.xml new file mode 100644 index 000000000..3312cc548 --- /dev/null +++ b/samples/react-webhooks-realtime/DeploySPFxToAppCatalogRequestBody.xml @@ -0,0 +1,148 @@ + + + + + IsClientSideSolutionDeployed + true + + + + + IsClientSideSolutionCurrentVersionDeployed + true + + + + + SkipFeatureDeployment + {skipFeatureDeployment} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/react-webhooks-realtime/DeploySPFxToO365CDN.ps1 b/samples/react-webhooks-realtime/DeploySPFxToO365CDN.ps1 new file mode 100644 index 000000000..6e00a9b27 --- /dev/null +++ b/samples/react-webhooks-realtime/DeploySPFxToO365CDN.ps1 @@ -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 +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/ProvisioningArtifacts.ps1 b/samples/react-webhooks-realtime/ProvisioningArtifacts.ps1 new file mode 100644 index 000000000..c728a45fb --- /dev/null +++ b/samples/react-webhooks-realtime/ProvisioningArtifacts.ps1 @@ -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 \ No newline at end of file diff --git a/samples/react-webhooks-realtime/README.md b/samples/react-webhooks-realtime/README.md new file mode 100644 index 000000000..92dd9a630 --- /dev/null +++ b/samples/react-webhooks-realtime/README.md @@ -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. + + \ No newline at end of file diff --git a/samples/react-webhooks-realtime/assets/Architecture.png b/samples/react-webhooks-realtime/assets/Architecture.png new file mode 100644 index 000000000..aa568e54f Binary files /dev/null and b/samples/react-webhooks-realtime/assets/Architecture.png differ diff --git a/samples/react-webhooks-realtime/assets/spfx-react-webhooks-realtime.gif b/samples/react-webhooks-realtime/assets/spfx-react-webhooks-realtime.gif new file mode 100644 index 000000000..01e2ac1ee Binary files /dev/null and b/samples/react-webhooks-realtime/assets/spfx-react-webhooks-realtime.gif differ diff --git a/samples/react-webhooks-realtime/config/config.json b/samples/react-webhooks-realtime/config/config.json new file mode 100644 index 000000000..dd99cead6 --- /dev/null +++ b/samples/react-webhooks-realtime/config/config.json @@ -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" + } +} diff --git a/samples/react-webhooks-realtime/config/copy-assets.json b/samples/react-webhooks-realtime/config/copy-assets.json new file mode 100644 index 000000000..e1bb26179 --- /dev/null +++ b/samples/react-webhooks-realtime/config/copy-assets.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json", + "deployCdnPath": "temp/deploy" +} diff --git a/samples/react-webhooks-realtime/config/deploy-azure-storage.json b/samples/react-webhooks-realtime/config/deploy-azure-storage.json new file mode 100644 index 000000000..19b84bb25 --- /dev/null +++ b/samples/react-webhooks-realtime/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./temp/deploy/", + "account": "", + "container": "react-webhooks-realtime", + "accessKey": "" +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/config/package-solution.json b/samples/react-webhooks-realtime/config/package-solution.json new file mode 100644 index 000000000..c76c3fafb --- /dev/null +++ b/samples/react-webhooks-realtime/config/package-solution.json @@ -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" + } +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/config/serve.json b/samples/react-webhooks-realtime/config/serve.json new file mode 100644 index 000000000..0eb6d456c --- /dev/null +++ b/samples/react-webhooks-realtime/config/serve.json @@ -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/" + } +} diff --git a/samples/react-webhooks-realtime/config/tslint.json b/samples/react-webhooks-realtime/config/tslint.json new file mode 100644 index 000000000..0bb934c20 --- /dev/null +++ b/samples/react-webhooks-realtime/config/tslint.json @@ -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 + } + } +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/config/write-manifests.json b/samples/react-webhooks-realtime/config/write-manifests.json new file mode 100644 index 000000000..c59af45d9 --- /dev/null +++ b/samples/react-webhooks-realtime/config/write-manifests.json @@ -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" +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/gulpfile.js b/samples/react-webhooks-realtime/gulpfile.js new file mode 100644 index 000000000..7d36ddb1c --- /dev/null +++ b/samples/react-webhooks-realtime/gulpfile.js @@ -0,0 +1,6 @@ +'use strict'; + +const gulp = require('gulp'); +const build = require('@microsoft/sp-build-web'); + +build.initialize(gulp); diff --git a/samples/react-webhooks-realtime/package.json b/samples/react-webhooks-realtime/package.json new file mode 100644 index 000000000..6b710de6b --- /dev/null +++ b/samples/react-webhooks-realtime/package.json @@ -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" + } +} diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/RealTimeListWebPart.manifest.json b/samples/react-webhooks-realtime/src/webparts/realTimeList/RealTimeListWebPart.manifest.json new file mode 100644 index 000000000..7466b2d9c --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/RealTimeListWebPart.manifest.json @@ -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" + } + }] +} diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/RealTimeListWebPart.ts b/samples/react-webhooks-realtime/src/webparts/realTimeList/RealTimeListWebPart.ts new file mode 100644 index 000000000..d99e803a5 --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/RealTimeListWebPart.ts @@ -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 { + + // Ovverriding onInit in order to set up the sp pnp js with the web part context + public onInit(): Promise { + return super.onInit().then(_ => { + // establish SPFx context + pnp.setup({ + spfxContext: this.context + }); + }); + } + + public render(): void { + const element: React.ReactElement = 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; + } +} diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/components/IRealTimeListProps.ts b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/IRealTimeListProps.ts new file mode 100644 index 000000000..bf64abdf9 --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/IRealTimeListProps.ts @@ -0,0 +1,4 @@ +export interface IRealTimeListProps { + socketserverurl: string; + siteUrl: string; +} diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/components/IRealTimeListState.ts b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/IRealTimeListState.ts new file mode 100644 index 000000000..e0b5959c5 --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/IRealTimeListState.ts @@ -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; +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/components/RealTimeList.module.scss b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/RealTimeList.module.scss new file mode 100644 index 000000000..56b91dc4a --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/RealTimeList.module.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/components/RealTimeList.tsx b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/RealTimeList.tsx new file mode 100644 index 000000000..d39df3fa0 --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/components/RealTimeList.tsx @@ -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 { + 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 { + if (this.props.siteUrl.toLowerCase().indexOf("wwww.contoso.com") >= 0 + || this.props.socketserverurl === undefined || this.props.socketserverurl === "") { + return ( +
+
+
+
+ Connect the web part with SharePoint and configuring it before to begin +
+
+
+
+ ); + } + let { sortedItems, columns } = this.state; + const loading: JSX.Element = (this.state.loading == true) ? : null; + const list: JSX.Element = (this.state.loading == false) ? + + : null; + const newsFeed: JSX.Element = + this.state.newsFeedVisible == true ? + this._loadList()} + /> + : null; + + return ( +
+
+
+
+ {newsFeed} +
+
+
+
+ {loading} +
+
+
+
+ {list} +
+
+
+
+ ); + } + 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 { + 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 { + 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, 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 | 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 ; + + case 'name': + return {fieldContent}; + + case 'color': + return {fieldContent}; + + default: + return {fieldContent}; + } +} \ No newline at end of file diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/loc/en-us.js b/samples/react-webhooks-realtime/src/webparts/realTimeList/loc/en-us.js new file mode 100644 index 000000000..dc03c2b61 --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/loc/en-us.js @@ -0,0 +1,7 @@ +define([], function() { + return { + "PropertyPaneDescription": "Description", + "BasicGroupName": "Configuration Panel", + "SocketserverurlFieldLabel": "Insert the Socket.IO server URL" + } +}); \ No newline at end of file diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/loc/mystrings.d.ts b/samples/react-webhooks-realtime/src/webparts/realTimeList/loc/mystrings.d.ts new file mode 100644 index 000000000..874407089 --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/loc/mystrings.d.ts @@ -0,0 +1,10 @@ +declare interface IRealTimeListWebPartStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + SocketserverurlFieldLabel: string; +} + +declare module 'RealTimeListWebPartStrings' { + const strings: IRealTimeListWebPartStrings; + export = strings; +} diff --git a/samples/react-webhooks-realtime/src/webparts/realTimeList/test/RealTimeListWebPart.test.ts b/samples/react-webhooks-realtime/src/webparts/realTimeList/test/RealTimeListWebPart.test.ts new file mode 100644 index 000000000..4bf4d0fea --- /dev/null +++ b/samples/react-webhooks-realtime/src/webparts/realTimeList/test/RealTimeListWebPart.test.ts @@ -0,0 +1,9 @@ +/// + +import { assert } from 'chai'; + +describe('RealTimeListWebPart', () => { + it('should do something', () => { + assert.ok(true); + }); +}); diff --git a/samples/react-webhooks-realtime/tsconfig.json b/samples/react-webhooks-realtime/tsconfig.json new file mode 100644 index 000000000..20a531bae --- /dev/null +++ b/samples/react-webhooks-realtime/tsconfig.json @@ -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" + ] + } +} diff --git a/samples/react-webhooks-realtime/typings/@ms/odsp.d.ts b/samples/react-webhooks-realtime/typings/@ms/odsp.d.ts new file mode 100644 index 000000000..5a2404000 --- /dev/null +++ b/samples/react-webhooks-realtime/typings/@ms/odsp.d.ts @@ -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; \ No newline at end of file diff --git a/samples/react-webhooks-realtime/typings/tsd.d.ts b/samples/react-webhooks-realtime/typings/tsd.d.ts new file mode 100644 index 000000000..e7efdd728 --- /dev/null +++ b/samples/react-webhooks-realtime/typings/tsd.d.ts @@ -0,0 +1 @@ +///