Merge pull request #1067 from joaojmendes/My-Tasks
React - My Tasks web part
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-no-unknown": null,
|
||||
"scss/at-rule-no-unknown": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.9.1",
|
||||
"libraryName": "react-my-task",
|
||||
"libraryId": "7cd3a77c-8a8d-4742-9585-e17ca19bfe5d",
|
||||
"environment": "spo",
|
||||
"packageManager": "npm",
|
||||
"framework": "react",
|
||||
"isCreatingSolution": true,
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
# React My Tasks Web Part
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
This web part allows user to manage planner tasks in SharePoint Site. The UI was inspired on Planner UI, it is full implemented with Office-UI-Fabric Components. Use MSGraph API's and PnPjs to data access.
|
||||
|
||||
The user can search task by name, can filter by progress status, all data are dynamic updated on change.
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![MyTasks](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/MyTasks.gif)
|
||||
|
||||
|
||||
|
||||
## List of Task Cards
|
||||
|
||||
|
||||
|
||||
![MyTasks](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen1.png)
|
||||
|
||||
## Filter Tasks
|
||||
|
||||
|
||||
![MyTasks](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen2.png)
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen3.png)
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen4.png)
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen5.png)
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
## Add Task
|
||||
|
||||
![MyTasks](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/AddTask.gif)
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen6.png)
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen7.png)
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
## Edit Tasks
|
||||
|
||||
|
||||
![MyTasks](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/EditTask.gif)
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen8.png)
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen9.png)
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen10.png)
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen11.png)
|
||||
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen12.png)
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen13.png)
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
|
||||
|
||||
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/My-Tasks/samples/react-mytasks/assets/screen14.png)
|
||||
|
||||
|
||||
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/version-1.9.1-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)
|
||||
|
||||
## WebPart Properties
|
||||
|
||||
Property |Type|Required| comments
|
||||
--------------------|----|--------|----------
|
||||
WebPart Title| Text| no|
|
||||
|
||||
|
||||
## Solution
|
||||
The Web Part Use PnPjs library, Office-ui-fabric-react components and MSGraph API's
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
My Tasks |João Mendes
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0|November 17, 2019|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
|
||||
- Move to sample folder
|
||||
- in the command line run:
|
||||
- `npm install`
|
||||
- `gulp build`
|
||||
- `gulp bundle --ship`
|
||||
- `gulp package-solution --ship`
|
||||
- `Add to AppCatalog and deploy`
|
||||
- `go to SharePoint Admin Center and Approve required API Permissions`
|
||||
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-MyTask" />
|
After Width: | Height: | Size: 23 MiB |
After Width: | Height: | Size: 46 MiB |
After Width: | Height: | Size: 20 MiB |
After Width: | Height: | Size: 590 KiB |
|
@ -0,0 +1 @@
|
|||
<svg height="30" viewBox="0 0 30 30" width="30" xmlns="http://www.w3.org/2000/svg"><path d="M19.5 7c-.657 0-.657 1 0 1H22c3.887 0 7 3.113 7 7s-3.113 7-7 7h-2c-3.887 0-7-3.113-7-7v-.5c0-.672-1-.648-1 0v.5c0 4.423 3.577 8 8 8h2c4.423 0 8-3.577 8-8s-3.577-8-8-8zM8 7c-4.423 0-8 3.577-8 8s3.577 8 8 8h2.5c.665 0 .66-1 0-1H8c-3.887 0-7-3.113-7-7s3.113-7 7-7h2c3.887 0 7 3.113 7 7v.5c0 .676 1 .656 1 0V15c0-4.423-3.577-8-8-8z"/></svg>
|
After Width: | Height: | Size: 428 B |
After Width: | Height: | Size: 385 KiB |
After Width: | Height: | Size: 504 KiB |
After Width: | Height: | Size: 495 KiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 516 KiB |
After Width: | Height: | Size: 518 KiB |
After Width: | Height: | Size: 390 KiB |
After Width: | Height: | Size: 358 KiB |
After Width: | Height: | Size: 348 KiB |
After Width: | Height: | Size: 442 KiB |
After Width: | Height: | Size: 441 KiB |
After Width: | Height: | Size: 538 KiB |
After Width: | Height: | Size: 528 KiB |
|
@ -0,0 +1,78 @@
|
|||
resources:
|
||||
- repo: self
|
||||
trigger:
|
||||
- master
|
||||
- develop
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
demands:
|
||||
- npm
|
||||
- node.js
|
||||
|
||||
steps:
|
||||
#install node 10.x
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 10.x'
|
||||
inputs:
|
||||
versionSpec: 10.x
|
||||
checkLatest: true
|
||||
|
||||
#install nodejs modules with npm
|
||||
- task: Npm@1
|
||||
displayName: 'npm install'
|
||||
inputs:
|
||||
workingDir: '$(Build.SourcesDirectory)'
|
||||
verbose: false
|
||||
|
||||
#start unit tests
|
||||
- task: Npm@1
|
||||
displayName: 'npm test'
|
||||
inputs:
|
||||
command: custom
|
||||
customCommand: 'test'
|
||||
workingDir: '$(Build.SourcesDirectory)'
|
||||
verbose: false
|
||||
|
||||
# Publish Test Results to Azure Pipelines/TFS
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: 'temp/test/junit/junit.xml'
|
||||
searchFolder: '$(Build.SourcesDirectory)'
|
||||
|
||||
# publish coverage test results
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: 'Publish Code Coverage Results $(Build.SourcesDirectory)/temp/test/cobertura-coverage.xml'
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: '$(Build.SourcesDirectory)/temp/test/cobertura-coverage.xml'
|
||||
reportDirectory: '$(Build.SourcesDirectory)/temp/test/'
|
||||
|
||||
#bundle code with gulp
|
||||
- task: Gulp@0
|
||||
displayName: 'gulp bundle'
|
||||
inputs:
|
||||
gulpFile: '$(Build.SourcesDirectory)/gulpfile.js'
|
||||
targets: bundle
|
||||
arguments: '--ship'
|
||||
continueOnError: true
|
||||
|
||||
#package solution with gulp
|
||||
- task: Gulp@0
|
||||
displayName: 'gulp package-solution'
|
||||
inputs:
|
||||
gulpFile: '$(Build.SourcesDirectory)/gulpfile.js'
|
||||
targets: 'package-solution'
|
||||
arguments: '--ship'
|
||||
|
||||
#copy files to artifact repository
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy Files to: $(build.artifactstagingdirectory)/drop'
|
||||
inputs:
|
||||
Contents: '**\*.sppkg'
|
||||
TargetFolder: '$(build.artifactstagingdirectory)/drop'
|
||||
|
||||
#publish artifacts
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: drop'
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/drop'
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"my-tasks-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/myTasks/MyTasksWebPart.js",
|
||||
"manifest": "./src/webparts/myTasks/MyTasksWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"MyTasksWebPartStrings": "lib/webparts/myTasks/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"includeExtensions": [
|
||||
"png",
|
||||
"jpg",
|
||||
"svg"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-my-task",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"preset":"@voitanos/jest-preset-spfx-react16","rootDir":"../src","coverageReporters":["text","json","lcov","text-summary","cobertura"],"reporters":["default","jest-junit"]}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-my-task-client-side-solution",
|
||||
"id": "7cd3a77c-8a8d-4742-9585-e17ca19bfe5d",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.ReadWrite.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.ReadWrite.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.AccessAsUser.All"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-my-task.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.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/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
'use strict';
|
||||
|
||||
// check if gulp dist was called
|
||||
if (process.argv.indexOf('dist') !== -1) {
|
||||
// add ship options to command call
|
||||
process.argv.push('--ship');
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
const gulpSequence = require('gulp-sequence');
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
// Create clean distrubution package
|
||||
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
|
||||
// Create clean development package
|
||||
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Webpack Bundle Anlayzer
|
||||
* Reference and gulp task
|
||||
*/
|
||||
const bundleAnalyzer = require('webpack-bundle-analyzer');
|
||||
|
||||
build.configureWebpack.mergeConfig({
|
||||
|
||||
additionalConfiguration: (generatedConfiguration) => {
|
||||
const lastDirName = path.basename(__dirname);
|
||||
const dropPath = path.join(__dirname, 'temp', 'stats');
|
||||
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
|
||||
openAnalyzer: false,
|
||||
analyzerMode: 'static',
|
||||
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
|
||||
generateStatsFile: true,
|
||||
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
|
||||
logLevel: 'error'
|
||||
}));
|
||||
|
||||
return generatedConfiguration;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* StyleLinter configuration
|
||||
* Reference and custom gulp task
|
||||
*/
|
||||
const stylelint = require('gulp-stylelint');
|
||||
|
||||
/* Stylelinter sub task */
|
||||
let styleLintSubTask = build.subTask('stylelint', (gulp) => {
|
||||
|
||||
console.log('[stylelint]: By default style lint errors will not break your build. If you want to change this behaviour, modify failAfterError parameter in gulpfile.js.');
|
||||
|
||||
return gulp
|
||||
.src('src/**/*.scss')
|
||||
.pipe(stylelint({
|
||||
failAfterError: false,
|
||||
reporters: [{
|
||||
formatter: 'string',
|
||||
console: true
|
||||
}]
|
||||
}));
|
||||
});
|
||||
/* end sub task */
|
||||
|
||||
build.rig.addPreBuildTask(styleLintSubTask);
|
||||
|
||||
/**
|
||||
* Custom Framework Specific gulp tasks
|
||||
*/
|
||||
|
||||
|
||||
build.initialize(gulp);
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "react-my-task",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"preversion": "node ./tools/pre-version.js",
|
||||
"postversion": "gulp dist",
|
||||
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
|
||||
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.9.1",
|
||||
"@microsoft/sp-http": "^1.9.1",
|
||||
"@microsoft/sp-lodash-subset": "1.9.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
|
||||
"@microsoft/sp-property-pane": "1.9.1",
|
||||
"@microsoft/sp-webpart-base": "1.9.1",
|
||||
"@pnp/common": "^1.3.6",
|
||||
"@pnp/graph": "^1.3.6",
|
||||
"@pnp/logging": "^1.3.6",
|
||||
"@pnp/odata": "^1.3.6",
|
||||
"@pnp/pnpjs": "^1.3.5",
|
||||
"@pnp/sp": "^1.3.6",
|
||||
"@pnp/spfx-controls-react": "1.13.2",
|
||||
"@pnp/spfx-property-controls": "1.15.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/jquery": "^3.3.30",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"@uifabric/fluent-theme": "^0.16.21",
|
||||
"date-fns": "^2.5.1",
|
||||
"jquery": "^3.4.1",
|
||||
"moment": "^2.24.0",
|
||||
"office-ui-fabric-react": "^6.209.0",
|
||||
"react": "16.8.5",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "16.8.5",
|
||||
"react-infinite-scroller": "^1.2.4",
|
||||
"tslint": "^5.20.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^1.12.0",
|
||||
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
|
||||
"@microsoft/rush-stack-compiler-3.3": "^0.2.37",
|
||||
"@microsoft/sp-build-web": "1.9.1",
|
||||
"@microsoft/sp-module-interfaces": "1.9.1",
|
||||
"@microsoft/sp-tslint-rules": "1.9.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.9.1",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"@types/react": "16.8.8",
|
||||
"@voitanos/jest-preset-spfx-react16": "^1.3.2",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1",
|
||||
"gulp-sequence": "1.0.0",
|
||||
"gulp-stylelint": "^8.0.0",
|
||||
"jest": "^23.6.0",
|
||||
"jest-junit": "^6.3.0",
|
||||
"stylelint": "^10.1.0",
|
||||
"stylelint-config-standard": "^18.3.0",
|
||||
"stylelint-scss": "^3.11.1",
|
||||
"typescript": "~3.3.x",
|
||||
"webpack-bundle-analyzer": "^3.6.0"
|
||||
},
|
||||
"jest-junit": {
|
||||
"output": "temp/test/junit/junit.xml",
|
||||
"usePathForSuiteName": "true"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import * as React from 'react';
|
||||
import styles from './AddLink.module.scss';
|
||||
import { IAddLinkProps } from './IAddLinkProps';
|
||||
import { IAddLinkState } from './IAddLinkState';
|
||||
import {
|
||||
Stack,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogType,
|
||||
DialogFooter,
|
||||
PrimaryButton,
|
||||
DefaultButton,
|
||||
} from 'office-ui-fabric-react';
|
||||
import * as tsStyles from './AddLinkStyles';
|
||||
import { ITaskExternalReference } from '../../services/ITaskExternalReference';
|
||||
import { ITaskDetails } from '../../services/ITaskDetails';
|
||||
import { utilities } from '../../utilities';
|
||||
import * as strings from 'MyTasksWebPartStrings';
|
||||
|
||||
export class AddLink extends React.Component<IAddLinkProps, IAddLinkState> {
|
||||
private _newReferences: ITaskExternalReference = {} as ITaskExternalReference;
|
||||
private _taskDetails:ITaskDetails = {} as ITaskDetails;
|
||||
private _util = new utilities();
|
||||
|
||||
constructor(props: IAddLinkProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hideDialog: !this.props.displayDialog,
|
||||
disableSaveButton: true,
|
||||
link:'',
|
||||
linkLabel:'',
|
||||
|
||||
};
|
||||
|
||||
this._taskDetails = this.props.taskDetails;
|
||||
}
|
||||
|
||||
private _closeDialog = (ev?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
ev.preventDefault();
|
||||
this.setState({ hideDialog: true });
|
||||
this.props.onDismiss(this._taskDetails);
|
||||
}
|
||||
|
||||
|
||||
private _onSave = async (ev?: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>{
|
||||
try {
|
||||
let { link, linkLabel } = this.state;
|
||||
const hasHttps = link.indexOf('https://') !== -1 ? true : false;
|
||||
if (!hasHttps){
|
||||
link = `https://${link}`;
|
||||
}
|
||||
|
||||
const fileFullUrl: string =
|
||||
(`${decodeURIComponent(link)}`).replace(/\./g, '%2E').replace(/\:/g, '%3A') + '?web=1';
|
||||
this._newReferences[fileFullUrl] = {
|
||||
alias: linkLabel ? linkLabel : link,
|
||||
'@odata.type': '#microsoft.graph.plannerExternalReference',
|
||||
type: await this._util.getFileType(link),
|
||||
previewPriority: ' !'
|
||||
};
|
||||
|
||||
for (const referenceKey of Object.keys(this._taskDetails.references)) {
|
||||
const originalReference = this._taskDetails.references[referenceKey];
|
||||
this._newReferences[referenceKey] = {
|
||||
alias: originalReference.alias,
|
||||
'@odata.type': '#microsoft.graph.plannerExternalReference',
|
||||
type: originalReference.type,
|
||||
previewPriority: ' !'
|
||||
};
|
||||
}
|
||||
|
||||
const updatedTaskDetails = await this.props.spservice.updateTaskDetailsProperty(
|
||||
this.props.taskDetails.id,
|
||||
'References',
|
||||
this._newReferences,
|
||||
this.props.taskDetails['@odata.etag']
|
||||
);
|
||||
this._taskDetails = updatedTaskDetails ;
|
||||
// this._taskDetails.references = this._newReferences;
|
||||
this.setState({ hideDialog: true });
|
||||
this.props.onDismiss(this._taskDetails);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
private _onUrlCHange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue:string) => {
|
||||
this.setState({disableSaveButton : newValue.length > 0 ? false : true, link: newValue});
|
||||
}
|
||||
|
||||
private _onChangeLabel = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue:string) => {
|
||||
this.setState({ linkLabel: newValue});
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IAddLinkProps> {
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
hidden={this.state.hideDialog}
|
||||
onDismiss={this._closeDialog}
|
||||
minWidth={430}
|
||||
maxWidth={430}
|
||||
dialogContentProps={{
|
||||
type: DialogType.normal,
|
||||
title: strings.AddLinkLabel
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
styles: tsStyles.modalStyles
|
||||
// topOffsetFixed: true
|
||||
}}>
|
||||
<Stack gap='20'>
|
||||
<TextField // prettier-ignore
|
||||
label={strings.AddressLabel}
|
||||
placeholder={strings.LinkWebAddressPlaceHolder}
|
||||
prefix='https://'
|
||||
borderless
|
||||
ariaLabel='Url'
|
||||
onChange={this._onUrlCHange}
|
||||
styles={tsStyles.textFielUrlStyles}
|
||||
/>
|
||||
<TextField // prettier-ignore
|
||||
label={strings.TextToDisplayLabel}
|
||||
placeholder= {strings.LinkNameHerePlaceHolder}
|
||||
ariaLabel={strings.AddressLabel}
|
||||
borderless
|
||||
onChange={this._onChangeLabel}
|
||||
styles={tsStyles.textFielUrlStyles}
|
||||
/>
|
||||
</Stack>
|
||||
<div style={{ marginTop: 45 }}>
|
||||
<DialogFooter>
|
||||
<PrimaryButton onClick={this._onSave} text={strings.SaveLabel} disabled={this.state.disableSaveButton} />
|
||||
<DefaultButton onClick={this._closeDialog} text={strings.CancelLabel} />
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import { FontSizes, FontWeights, DefaultPalette } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { CommunicationColors } from '@uifabric/fluent-theme/lib/fluent/FluentColors';
|
||||
import {
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
IStackItemStyles,
|
||||
ITextFieldStyles,
|
||||
ITextFieldSubComponentStyles,
|
||||
IModalStyles,
|
||||
ImageLoadState,
|
||||
IDatePickerStyles,
|
||||
ITextFieldProps,
|
||||
IStyle,
|
||||
IButtonStyles,
|
||||
calculatePrecision,
|
||||
IDropdownStyles,
|
||||
} from 'office-ui-fabric-react';
|
||||
import { getTheme } from '@uifabric/styling';
|
||||
|
||||
// Styles definition
|
||||
export const stackStyles: IStackStyles = {
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}
|
||||
};
|
||||
export const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
padding: 5,
|
||||
display: 'flex',
|
||||
width: 172,
|
||||
height: 32,
|
||||
fontWeight: FontWeights.regular,
|
||||
}
|
||||
};
|
||||
|
||||
export const stackTokens: IStackTokens = {
|
||||
childrenGap: 10,
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const textFielUrlStyles: ITextFieldStyles = {
|
||||
field: { backgroundColor: `${DefaultPalette.neutralLighter} !important` },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
|
||||
export const modalStyles: IModalStyles = {
|
||||
main: { minWidth: 400 ,maxWidth: 450, },
|
||||
root: {},
|
||||
keyboardMoveIcon: {},
|
||||
keyboardMoveIconContainer: {},
|
||||
layer: {},
|
||||
scrollableContent: {}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { ITaskExternalReference } from "../../services/ITaskExternalReference";
|
||||
import spservices from "../../services/spservices";
|
||||
import { ITaskDetails } from "../../services/ITaskDetails";
|
||||
|
||||
export interface IAddLinkProps {
|
||||
onDismiss?: (references:ITaskDetails) => void;
|
||||
displayDialog: boolean;
|
||||
taskDetails:ITaskDetails;
|
||||
spservice: spservices;
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ITaskDetails } from "../../services/ITaskDetails";
|
||||
|
||||
export interface IAddLinkState {
|
||||
hideDialog:boolean;
|
||||
disableSaveButton:boolean;
|
||||
link:string;
|
||||
linkLabel:string;
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './AddLink';
|
|
@ -0,0 +1,329 @@
|
|||
import * as React from 'react';
|
||||
import { IEditCategoriesProps } from './IEditCategoriesProps';
|
||||
import { IEditCategoriesState } from './IEditCategoriesState';
|
||||
import { IPlannerPlanExtended } from '../../services/IPlannerPlanExtended';
|
||||
import { IPlannerPlanDetails } from '../../services/IPlannerPlanDetails';
|
||||
import {
|
||||
Stack,
|
||||
Checkbox,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
IconButton
|
||||
} from 'office-ui-fabric-react';
|
||||
import { IAppliedCategories } from '../../services/IAppliedCategories';
|
||||
import { getTheme } from '@uifabric/styling';
|
||||
import { textFieldSearchStyles } from '../UploadFile/UploadStyles';
|
||||
import * as tsStyles from './EditCategoriesStyles';
|
||||
|
||||
const categoriesColors = {
|
||||
category1: '#e000f1',
|
||||
category2: '#f44b1d',
|
||||
category3: '#e39e27',
|
||||
category4: '#aee01e',
|
||||
category5: '#46A08E',
|
||||
category6: '#62cef0'
|
||||
};
|
||||
|
||||
export class EditCategories extends React.Component<IEditCategoriesProps, IEditCategoriesState> {
|
||||
private _plannerPlanDetails: IPlannerPlanDetails;
|
||||
private _appliedCategoryKeys: IAppliedCategories = {};
|
||||
|
||||
constructor(props: IEditCategoriesProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
task: this.props.task,
|
||||
appliedCategories: [] as JSX.Element[],
|
||||
hasError: false,
|
||||
errorMessage: '',
|
||||
category1Value: this.props.task.appliedCategories["category1"],
|
||||
category2Value: this.props.task.appliedCategories["category2"],
|
||||
category3Value: this.props.task.appliedCategories["category3"],
|
||||
category4Value: this.props.task.appliedCategories["category4"],
|
||||
category5Value: this.props.task.appliedCategories["category5"],
|
||||
category6Value: this.props.task.appliedCategories["category6"],
|
||||
plannerDetails : {} as IPlannerPlanDetails,
|
||||
};
|
||||
}
|
||||
|
||||
public async componentWillMount(): Promise<void> {
|
||||
|
||||
this._appliedCategoryKeys = this.props.task.appliedCategories;
|
||||
this._plannerPlanDetails = await this.props.spservice.getPlanDetails(this.props.task.planId);
|
||||
// const allCategoriesKeys = Object.keys(categoriesColors);
|
||||
// const appliedCategories: JSX.Element[] = [];
|
||||
let categoriesCheched: {[key:string]: boolean} = {};
|
||||
|
||||
this.setState({ plannerDetails: this._plannerPlanDetails});
|
||||
}
|
||||
|
||||
|
||||
public async componentDidUpdate(prevProps: IEditCategoriesProps, prevState: IEditCategoriesState): Promise<void> {
|
||||
if (!this._plannerPlanDetails){
|
||||
this._plannerPlanDetails = await this.props.spservice.getPlanDetails(this.props.task.planId);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines whether check box category changed on
|
||||
*/
|
||||
private _onCheckBoxCategory1Changed = async (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
ev.preventDefault();
|
||||
|
||||
|
||||
try {
|
||||
this._appliedCategoryKeys["category1"] = checked;
|
||||
const updatedTask = await this.props.spservice.updateTaskProperty(
|
||||
this.state.task.id,
|
||||
'appliedCategories',
|
||||
this._appliedCategoryKeys,
|
||||
this.state.task['@odata.etag']
|
||||
);
|
||||
this.setState({task: updatedTask , category1Value: checked});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
private _onCheckBoxCategory2Changed = async (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
this._appliedCategoryKeys["category2"] = checked;
|
||||
const updatedTask = await this.props.spservice.updateTaskProperty(
|
||||
this.state.task.id,
|
||||
'appliedCategories',
|
||||
this._appliedCategoryKeys,
|
||||
this.state.task['@odata.etag']
|
||||
);
|
||||
this.setState({task: updatedTask , category2Value: checked});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
private _onCheckBoxCategory3Changed = async (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
ev.preventDefault();
|
||||
|
||||
|
||||
try {
|
||||
this._appliedCategoryKeys["category3"] = checked;
|
||||
const updatedTask = await this.props.spservice.updateTaskProperty(
|
||||
this.state.task.id,
|
||||
'appliedCategories',
|
||||
this._appliedCategoryKeys,
|
||||
this.state.task['@odata.etag']
|
||||
);
|
||||
this.setState({task: updatedTask , category3Value: checked});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
private _onCheckBoxCategory5Changed = async (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
this._appliedCategoryKeys["category5"] = checked;
|
||||
const updatedTask = await this.props.spservice.updateTaskProperty(
|
||||
this.state.task.id,
|
||||
'appliedCategories',
|
||||
this._appliedCategoryKeys,
|
||||
this.state.task['@odata.etag']
|
||||
);
|
||||
this.setState({task: updatedTask , category5Value: checked});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
private _onCheckBoxCategory6Changed = async (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
this._appliedCategoryKeys["category6"] = checked;
|
||||
const updatedTask = await this.props.spservice.updateTaskProperty(
|
||||
this.state.task.id,
|
||||
'appliedCategories',
|
||||
this._appliedCategoryKeys,
|
||||
this.state.task['@odata.etag']
|
||||
);
|
||||
this.setState({task: updatedTask , category6Value: checked});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
private _onCheckBoxCategory4Changed = async (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
this._appliedCategoryKeys["category4"] = checked;
|
||||
const updatedTask = await this.props.spservice.updateTaskProperty(
|
||||
this.state.task.id,
|
||||
'appliedCategories',
|
||||
this._appliedCategoryKeys,
|
||||
this.state.task['@odata.etag']
|
||||
);
|
||||
this.setState({task: updatedTask , category4Value: checked});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Renders edit categories
|
||||
* @returns render
|
||||
*/
|
||||
public render(): React.ReactElement<IEditCategoriesProps> {
|
||||
return (
|
||||
<div>
|
||||
<Stack horizontal horizontalAlign='start' gap={10}>
|
||||
{this.state.hasError ? (
|
||||
<MessageBar messageBarType={MessageBarType.error}>{this.state.errorMessage}</MessageBar>
|
||||
) : (
|
||||
|
||||
<>
|
||||
<div title={
|
||||
this.state.plannerDetails && this.state.plannerDetails.categoryDescriptions
|
||||
? this.state.plannerDetails.categoryDescriptions["category1"]
|
||||
: ''
|
||||
}>
|
||||
<Checkbox
|
||||
|
||||
onChange={this._onCheckBoxCategory1Changed}
|
||||
checked={ this.state.category1Value}
|
||||
styles={{
|
||||
...tsStyles.checkboxStyles,
|
||||
label: { selectors: { [':hover .ms-Checkbox-checkbox']: { background: categoriesColors["category1"] } } },
|
||||
checkbox: {
|
||||
selectors: { [':hover']: { backgroundColor: 'rgba(0, 0, 0, 0.2)' } },
|
||||
width: 90,
|
||||
height: 25,
|
||||
borderStyle: 'none',
|
||||
marginRight: 0,
|
||||
background: categoriesColors["category1"]
|
||||
}
|
||||
}}>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div title={
|
||||
this.state.plannerDetails && this.state.plannerDetails.categoryDescriptions
|
||||
? this.state.plannerDetails.categoryDescriptions["category2"]
|
||||
: ''
|
||||
}>
|
||||
<Checkbox
|
||||
|
||||
onChange={this._onCheckBoxCategory2Changed}
|
||||
checked={ this.state.category2Value}
|
||||
styles={{
|
||||
...tsStyles.checkboxStyles,
|
||||
label: { selectors: { [':hover .ms-Checkbox-checkbox']: { background: categoriesColors["category2"] } } },
|
||||
checkbox: {
|
||||
selectors: { [':hover']: { backgroundColor: 'rgba(0, 0, 0, 0.2)' } },
|
||||
width: 90,
|
||||
height: 25,
|
||||
borderStyle: 'none',
|
||||
marginRight: 0,
|
||||
background: categoriesColors["category2"]
|
||||
}
|
||||
}}>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div title={
|
||||
this.state.plannerDetails && this.state.plannerDetails.categoryDescriptions
|
||||
? this.state.plannerDetails.categoryDescriptions["category3"]
|
||||
: ''
|
||||
}>
|
||||
<Checkbox
|
||||
|
||||
onChange={this._onCheckBoxCategory3Changed}
|
||||
checked={ this.state.category3Value}
|
||||
styles={{
|
||||
...tsStyles.checkboxStyles,
|
||||
label: { selectors: { [':hover .ms-Checkbox-checkbox']: { background: categoriesColors["category3"] } } },
|
||||
checkbox: {
|
||||
selectors: { [':hover']: { backgroundColor: 'rgba(0, 0, 0, 0.2)' } },
|
||||
width: 90,
|
||||
height: 25,
|
||||
borderStyle: 'none',
|
||||
marginRight: 0,
|
||||
|
||||
background: categoriesColors["category3"]
|
||||
}
|
||||
}}>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div title={
|
||||
this.state.plannerDetails && this.state.plannerDetails.categoryDescriptions
|
||||
? this.state.plannerDetails.categoryDescriptions["category4"]
|
||||
: ''
|
||||
}>
|
||||
<Checkbox
|
||||
|
||||
onChange={this._onCheckBoxCategory4Changed}
|
||||
checked={ this.state.category4Value}
|
||||
styles={{
|
||||
...tsStyles.checkboxStyles,
|
||||
label: { selectors: { [':hover .ms-Checkbox-checkbox']: { background: categoriesColors["category4"] } } },
|
||||
checkbox: {
|
||||
selectors: { [':hover']: { backgroundColor: 'rgba(0, 0, 0, 0.2)' } },
|
||||
width: 90,
|
||||
height: 25,
|
||||
borderStyle: 'none',
|
||||
marginRight: 0,
|
||||
background: categoriesColors["category4"]
|
||||
}
|
||||
}}>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div title={
|
||||
this.state.plannerDetails && this.state.plannerDetails.categoryDescriptions
|
||||
? this.state.plannerDetails.categoryDescriptions["category5"]
|
||||
: ''
|
||||
}>
|
||||
|
||||
<Checkbox
|
||||
|
||||
onChange={this._onCheckBoxCategory5Changed}
|
||||
checked={ this.state.category5Value}
|
||||
styles={{
|
||||
...tsStyles.checkboxStyles,
|
||||
label: { selectors: { [':hover .ms-Checkbox-checkbox']: { background: categoriesColors["category5"] } } },
|
||||
checkbox: {
|
||||
selectors: { [':hover']: { backgroundColor: 'rgba(0, 0, 0, 0.2)' } },
|
||||
width: 90,
|
||||
height: 25,
|
||||
borderStyle: 'none',
|
||||
marginRight: 0,
|
||||
background: categoriesColors["category5"]
|
||||
}
|
||||
}}>
|
||||
</Checkbox>
|
||||
</div >
|
||||
<div title={
|
||||
this.state.plannerDetails && this.state.plannerDetails.categoryDescriptions
|
||||
? this.state.plannerDetails.categoryDescriptions["category6"]
|
||||
: ''
|
||||
}>
|
||||
<Checkbox
|
||||
|
||||
onChange={this._onCheckBoxCategory6Changed}
|
||||
checked={ this.state.category6Value}
|
||||
styles={{
|
||||
...tsStyles.checkboxStyles,
|
||||
label: { selectors: { [':hover .ms-Checkbox-checkbox']: { background: categoriesColors["category6"] } } },
|
||||
checkbox: {
|
||||
selectors: { [':hover']: { backgroundColor: 'rgba(0, 0, 0, 0.2)' } },
|
||||
width: 90,
|
||||
height: 25,
|
||||
borderStyle: 'none',
|
||||
marginRight: 0,
|
||||
background: categoriesColors["category6"]
|
||||
}
|
||||
}}>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { FontSizes, FontWeights, DefaultPalette } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { CommunicationColors } from '@uifabric/fluent-theme/lib/fluent/FluentColors';
|
||||
import {
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
IStackItemStyles,
|
||||
ITextFieldStyles,
|
||||
ITextFieldSubComponentStyles,
|
||||
IModalStyles,
|
||||
ImageLoadState,
|
||||
IDatePickerStyles,
|
||||
ITextFieldProps,
|
||||
IStyle,
|
||||
IButtonStyles,
|
||||
calculatePrecision,
|
||||
IDropdownStyles,
|
||||
ICheckStyleProps,
|
||||
ICheckStyles,
|
||||
ICheckboxStyles
|
||||
} from 'office-ui-fabric-react';
|
||||
|
||||
// Styles definition
|
||||
export const stackStyles: IStackStyles = {
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}
|
||||
};
|
||||
export const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
padding: 5,
|
||||
display: 'flex',
|
||||
width: 172,
|
||||
height: 32,
|
||||
fontWeight: FontWeights.regular
|
||||
}
|
||||
};
|
||||
|
||||
export const stackTokens: IStackTokens = {
|
||||
childrenGap: 10
|
||||
};
|
||||
|
||||
export const textFielUrlStyles: ITextFieldStyles = {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const modalStyles: IModalStyles = {
|
||||
main: { minWidth: 400, maxWidth: 450 },
|
||||
root: {},
|
||||
keyboardMoveIcon: {},
|
||||
keyboardMoveIconContainer: {},
|
||||
layer: {},
|
||||
scrollableContent: {}
|
||||
};
|
||||
|
||||
export const checkboxStyles: ICheckboxStyles = {
|
||||
root: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
width: 90,
|
||||
height: 20,
|
||||
marginTop: 30,
|
||||
selectors: { [':hover']: { background: 'rgba(0, 0, 0, 0.2)' } }
|
||||
},
|
||||
input : {width: 90,
|
||||
height: 20,
|
||||
},
|
||||
checkmark : {
|
||||
fontSize : 16
|
||||
}
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { ITask } from "../../services/ITask";
|
||||
import spservice from './../../services/spservices';
|
||||
import { IPlannerPlanExtended } from "../../services/IPlannerPlanExtended";
|
||||
export interface IEditCategoriesProps {
|
||||
spservice: spservice;
|
||||
onDismiss?: (refresh:boolean) => void;
|
||||
task: ITask;
|
||||
plannerPlan: IPlannerPlanExtended;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { ITask } from "../../services/ITask";
|
||||
import { IAppliedCategories } from "../../services/IAppliedCategories";
|
||||
import { IPlannerPlanDetails } from "../../services/IPlannerPlanDetails";
|
||||
|
||||
export interface IEditCategoriesState {
|
||||
task: ITask;
|
||||
appliedCategories: JSX.Element[];
|
||||
hasError:boolean;
|
||||
errorMessage:string;
|
||||
category1Value: boolean;
|
||||
category2Value: boolean;
|
||||
category3Value: boolean;
|
||||
category4Value: boolean;
|
||||
category5Value: boolean;
|
||||
category6Value: boolean;
|
||||
plannerDetails: IPlannerPlanDetails;
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
import * as React from 'react';
|
||||
import { IEditLinkProps } from './IEditLinkProps';
|
||||
import { IEditLinkState } from './IEditLinkState';
|
||||
import {
|
||||
Stack,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogType,
|
||||
DialogFooter,
|
||||
PrimaryButton,
|
||||
DefaultButton,
|
||||
|
||||
} from 'office-ui-fabric-react';
|
||||
import * as tsStyles from './EditLinkStyles';
|
||||
import { ITaskExternalReference } from '../../services/ITaskExternalReference';
|
||||
import { ITaskDetails } from '../../services/ITaskDetails';
|
||||
import { utilities } from '../../utilities';
|
||||
import * as strings from 'MyTasksWebPartStrings';
|
||||
|
||||
export class EditLink extends React.Component<IEditLinkProps, IEditLinkState> {
|
||||
private _newReferences: ITaskExternalReference = {} as ITaskExternalReference;
|
||||
private _taskDetails:ITaskDetails = {} as ITaskDetails;
|
||||
private _originalReferenceKey:string = '';
|
||||
private _util = new utilities();
|
||||
constructor(props: IEditLinkProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hideDialog: !this.props.displayDialog,
|
||||
disableSaveButton: true,
|
||||
link:'',
|
||||
linkLabel:'',
|
||||
|
||||
};
|
||||
|
||||
this._taskDetails = this.props.taskDetails;
|
||||
|
||||
}
|
||||
|
||||
public componentWillMount(): void {
|
||||
const link = this.props.link;
|
||||
const fileFullUrl: string =(`${link}`).replace(/\./g, '%2E').replace(/\:/g, '%3A');
|
||||
const reference = this._taskDetails.references[fileFullUrl];
|
||||
this._originalReferenceKey = fileFullUrl;
|
||||
this.setState({link: link.replace('https%3A//','').replace('https://',''), linkLabel:reference.alias, disableSaveButton:false});
|
||||
}
|
||||
|
||||
private _closeDialog = (ev?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
this.setState({ hideDialog: true });
|
||||
this.props.onDismiss(this._taskDetails);
|
||||
}
|
||||
|
||||
|
||||
private _onSave = async (ev?: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>{
|
||||
try {
|
||||
let { link, linkLabel } = this.state;
|
||||
const hasHttps = link.indexOf('https://') !== -1 ? true : false;
|
||||
if (!hasHttps){
|
||||
link = `https://${link}`;
|
||||
}
|
||||
|
||||
const fileFullUrl: string =
|
||||
(`${decodeURIComponent(link)}`).replace(/\./g, '%2E').replace(/\:/g, '%3A')+ '?web=1';
|
||||
if ( this._originalReferenceKey !== fileFullUrl){
|
||||
delete this._taskDetails.references[this._originalReferenceKey];
|
||||
this._newReferences[this._originalReferenceKey] = null;
|
||||
}
|
||||
|
||||
this._taskDetails.references[fileFullUrl] = {
|
||||
alias: linkLabel ? linkLabel : link,
|
||||
'@odata.type': '#microsoft.graph.plannerExternalReference',
|
||||
type: await this._util.getFileType(link),
|
||||
previewPriority: ' !'
|
||||
};
|
||||
|
||||
for (const referenceKey of Object.keys(this._taskDetails.references)) {
|
||||
const originalReference = this._taskDetails.references[referenceKey];
|
||||
this._newReferences[referenceKey] = {
|
||||
alias: originalReference.alias,
|
||||
'@odata.type': '#microsoft.graph.plannerExternalReference',
|
||||
type: originalReference.type,
|
||||
previewPriority: ' !'
|
||||
};
|
||||
}
|
||||
|
||||
const updateTaskDetails = await this.props.spservice.updateTaskDetailsProperty(
|
||||
this.props.taskDetails.id,
|
||||
'References',
|
||||
this._newReferences,
|
||||
this.props.taskDetails['@odata.etag']
|
||||
);
|
||||
this._taskDetails = updateTaskDetails;
|
||||
// this._taskDetails.references = this._newReferences;
|
||||
this._closeDialog();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
private _onUrlCHange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue:string) => {
|
||||
this.setState({disableSaveButton : newValue.length > 0 ? false : true, link: newValue});
|
||||
}
|
||||
|
||||
private _onChangeLabel = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue:string) => {
|
||||
this.setState({ linkLabel: newValue});
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IEditLinkProps> {
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
hidden={this.state.hideDialog}
|
||||
onDismiss={this._closeDialog}
|
||||
minWidth={430}
|
||||
maxWidth={430}
|
||||
dialogContentProps={{
|
||||
type: DialogType.normal,
|
||||
title: strings.AddLinkLabel
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
styles: tsStyles.modalStyles
|
||||
// topOffsetFixed: true
|
||||
}}>
|
||||
<Stack gap='20'>
|
||||
<TextField // prettier-ignore
|
||||
label={strings.AddressLabel}
|
||||
placeholder={strings.LinkWebAddressPlaceHolder}
|
||||
value={this.state.link}
|
||||
prefix='https://'
|
||||
borderless
|
||||
ariaLabel='Url'
|
||||
onChange={this._onUrlCHange}
|
||||
styles={tsStyles.textFielUrlStyles}
|
||||
/>
|
||||
<TextField // prettier-ignore
|
||||
label={strings.TextToDisplayLabel}
|
||||
placeholder={strings.LinkNameHerePlaceHolder}
|
||||
|
||||
value={this.state.linkLabel}
|
||||
borderless
|
||||
onChange={this._onChangeLabel}
|
||||
styles={tsStyles.textFielUrlStyles}
|
||||
/>
|
||||
</Stack>
|
||||
<div style={{ marginTop: 45 }}>
|
||||
<DialogFooter>
|
||||
<PrimaryButton onClick={this._onSave} text={strings.SaveLabel} disabled={this.state.disableSaveButton} />
|
||||
<DefaultButton onClick={this._closeDialog} text={strings.CancelLabel} />
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { FontSizes, FontWeights, DefaultPalette } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { CommunicationColors } from '@uifabric/fluent-theme/lib/fluent/FluentColors';
|
||||
import {
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
IStackItemStyles,
|
||||
ITextFieldStyles,
|
||||
ITextFieldSubComponentStyles,
|
||||
IModalStyles,
|
||||
ImageLoadState,
|
||||
IDatePickerStyles,
|
||||
ITextFieldProps,
|
||||
IStyle,
|
||||
IButtonStyles,
|
||||
calculatePrecision,
|
||||
IDropdownStyles,
|
||||
ICheckStyleProps,
|
||||
} from 'office-ui-fabric-react';
|
||||
|
||||
// Styles definition
|
||||
export const stackStyles: IStackStyles = {
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}
|
||||
};
|
||||
export const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
padding: 5,
|
||||
display: 'flex',
|
||||
width: 172,
|
||||
height: 32,
|
||||
fontWeight: FontWeights.regular,
|
||||
}
|
||||
};
|
||||
|
||||
export const stackTokens: IStackTokens = {
|
||||
childrenGap: 10,
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const textFielUrlStyles: ITextFieldStyles = {
|
||||
field: { backgroundColor: `${DefaultPalette.neutralLighter} !important` },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
|
||||
export const modalStyles: IModalStyles = {
|
||||
main: { minWidth: 400 ,maxWidth: 450, },
|
||||
root: {},
|
||||
keyboardMoveIcon: {},
|
||||
keyboardMoveIconContainer: {},
|
||||
layer: {},
|
||||
scrollableContent: {}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { ITaskExternalReference } from "../../services/ITaskExternalReference";
|
||||
import spservices from "../../services/spservices";
|
||||
import { ITaskDetails } from "../../services/ITaskDetails";
|
||||
|
||||
export interface IEditLinkProps {
|
||||
onDismiss?: (references:ITaskDetails) => void;
|
||||
displayDialog: boolean;
|
||||
taskDetails:ITaskDetails;
|
||||
spservice: spservices;
|
||||
link:string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ITaskDetails } from "../../services/ITaskDetails";
|
||||
|
||||
export interface IEditLinkState {
|
||||
hideDialog:boolean;
|
||||
disableSaveButton:boolean;
|
||||
link:string;
|
||||
linkLabel:string;
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './EditLink';
|
|
@ -0,0 +1,30 @@
|
|||
import * as React from 'react';
|
||||
|
||||
|
||||
export interface IProgressIndicatorProps {
|
||||
|
||||
}
|
||||
|
||||
export interface IProgressIndicatorState {
|
||||
|
||||
}
|
||||
|
||||
export default class ProgressIndicator extends React.Component<IProgressIndicatorProps, IProgressIndicatorState> {
|
||||
constructor(props: IProgressIndicatorProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
public render(): React.ReactElement<IProgressIndicatorProps> {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import spservices from '../../services/spservices';
|
||||
import { ITaskExternalReference } from '../../services/ITaskExternalReference';
|
||||
import { ChunkedFileUploadProgressData } from '@pnp/sp';
|
||||
export interface IUploadFileProps {
|
||||
onFileUpload?: (file:File, groupDefaultLibrary:string) => void;
|
||||
spservice: spservices;
|
||||
groupId: string;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface IUploadFileState {
|
||||
isUploading:boolean;
|
||||
percent:number;
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import * as React from 'react';
|
||||
import styles from './UploadFile.module.scss';
|
||||
|
||||
import {IUploadFileProps} from './IUploadFileProps';
|
||||
import {IUploadFileState} from './IUploadFileState';
|
||||
import { Dialog, Stack, Icon, IconType, Label, ProgressIndicator } from 'office-ui-fabric-react';
|
||||
import * as tsStyles from './UploadStyles';
|
||||
import { ChunkedFileUploadProgressData, Web } from '@pnp/sp';
|
||||
import * as strings from 'MyTasksWebPartStrings';
|
||||
|
||||
let file :File = undefined;
|
||||
|
||||
export interface IUploadFileState {}
|
||||
|
||||
export class UploadFile extends React.Component<IUploadFileProps, IUploadFileState> {
|
||||
private fileInput;
|
||||
|
||||
constructor(props: IUploadFileProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isUploading:false,
|
||||
percent:0,
|
||||
|
||||
};
|
||||
|
||||
this.fileInput = React.createRef();
|
||||
}
|
||||
|
||||
private _fireUploadFile = () => {
|
||||
// fire click event
|
||||
this.fileInput.current.value = '';
|
||||
this.fileInput.current.click();
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async componentDidMount(): Promise<void> {
|
||||
this._fireUploadFile();
|
||||
}
|
||||
|
||||
|
||||
public componentDidUpdate(prevProps: IUploadFileProps, prevState: IUploadFileState): void {
|
||||
this._fireUploadFile();
|
||||
}
|
||||
/**
|
||||
* Add a new attachment
|
||||
*/
|
||||
private uploadFile = async (e: React.ChangeEvent<HTMLInputElement>) =>{
|
||||
e.preventDefault();
|
||||
|
||||
const reader = new FileReader();
|
||||
file = e.target.files[0];
|
||||
console.log("file name",file.name);
|
||||
try {
|
||||
// this.props.onFileUpload(file);
|
||||
const groupUrl = await this.props.spservice.getGrouoUrl(this.props.groupId);
|
||||
const groupDefaultLibrary = await this.props.spservice.getGroupDocumentLibraryUrl(this.props.groupId);
|
||||
const web = new Web(groupUrl);
|
||||
const serverRelativedocumentLibrary = groupDefaultLibrary.replace(location.origin, '');
|
||||
|
||||
const rs = await web.getFolderByServerRelativeUrl(serverRelativedocumentLibrary).files.addChunked(
|
||||
file.name,
|
||||
file,
|
||||
(data: ChunkedFileUploadProgressData) => {
|
||||
|
||||
this.setState({percent: data.currentPointer / data.fileSize, isUploading:true});
|
||||
console.log('File Upload chunked %', (data.currentPointer / data.fileSize) );
|
||||
},
|
||||
true
|
||||
);
|
||||
this.setState({percent: 1, isUploading:true});
|
||||
setTimeout(() => {
|
||||
this.setState({isUploading:false});
|
||||
this.props.onFileUpload(file, groupDefaultLibrary);
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.log('rs-e', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public render(): React.ReactElement<IUploadFileProps> {
|
||||
return (
|
||||
<div>
|
||||
<input id="file-picker"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
onChange={(event) => this.uploadFile(event)}
|
||||
|
||||
ref={this.fileInput} />
|
||||
{
|
||||
this.state.isUploading &&
|
||||
<Stack horizontalAlign="start" horizontal gap="10" style={{width:'100%'}}>
|
||||
<Icon iconType={IconType.Default} iconName={"CloudUpload"} className={tsStyles.classNames.iconUploadStyles} />
|
||||
<ProgressIndicator className={tsStyles.classNames.progressIndicatorStyles} label={file.name} description={`${strings.Uploading} ${Math.round(this.state.percent * 100)} %`} percentComplete={(this.state.percent)}></ProgressIndicator>
|
||||
</Stack>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
import { FontSizes, FontWeights, DefaultPalette, getTheme, mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { CommunicationColors } from '@uifabric/fluent-theme/lib/fluent/FluentColors';
|
||||
import {
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
IStackItemStyles,
|
||||
ITextFieldStyles,
|
||||
ITextFieldSubComponentStyles,
|
||||
IModalStyles,
|
||||
ImageLoadState,
|
||||
IDatePickerStyles,
|
||||
ITextFieldProps,
|
||||
IStyle,
|
||||
IButtonStyles,
|
||||
calculatePrecision,
|
||||
IDropdownStyles,
|
||||
IPersonaStyles,
|
||||
IIconStyles
|
||||
} from 'office-ui-fabric-react';
|
||||
|
||||
// Styles definition
|
||||
|
||||
export const memberPersonaStyle: IPersonaStyles = {
|
||||
root: { flexGrow: 7, padding: 5, cursor: 'pointer' },
|
||||
details: {},
|
||||
primaryText: {},
|
||||
secondaryText: {},
|
||||
tertiaryText: {},
|
||||
optionalText: {},
|
||||
textContent: {}
|
||||
};
|
||||
|
||||
export const classNames = mergeStyleSets({
|
||||
centerColumn: { display: 'flex', alignItems:'center', height:'100%'},
|
||||
fileIconHeaderIcon: {
|
||||
padding: 0,
|
||||
fontSize: '16px'
|
||||
},
|
||||
fileIconCell: {
|
||||
textAlign: 'center',
|
||||
selectors: {
|
||||
'&:before': {
|
||||
content: '.',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
height: '100%',
|
||||
width: '0px',
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
iconUploadStyles:{
|
||||
fontSize: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: 30,
|
||||
paddingLeft: 12,
|
||||
marginTop: 12
|
||||
},
|
||||
progressIndicatorStyles: {
|
||||
width: '100%',
|
||||
marginTop: 12
|
||||
}
|
||||
});
|
||||
|
||||
export const stackStyles: IStackStyles = {
|
||||
root: {
|
||||
width: '100%',
|
||||
paddingLeft: 12,
|
||||
}
|
||||
};
|
||||
export const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
padding: 5,
|
||||
display: 'flex',
|
||||
width: 172,
|
||||
height: 32,
|
||||
fontWeight: FontWeights.regular
|
||||
}
|
||||
};
|
||||
|
||||
export const stackTokens: IStackTokens = {
|
||||
childrenGap: 10
|
||||
};
|
||||
|
||||
export const textFielStartDateDatePickerStyles: ITextFieldProps = {
|
||||
styles: {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
}
|
||||
};
|
||||
|
||||
export const textFielDueDateDatePickerStyles: ITextFieldProps = {
|
||||
styles: {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
}
|
||||
};
|
||||
|
||||
export const textFieldSearchStyles: ITextFieldStyles = {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const textFieldCheckListItem: ITextFieldStyles = {
|
||||
field: {},
|
||||
root: { width: '100%', height: 32, marginRight: 15 },
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: { backgroundColor: 'white' },
|
||||
suffix: { backgroundColor: 'white' },
|
||||
wrapper: { selectors: { [':hover']: { borderWidth: 1, borderStyle: 'solid', borderColor: DefaultPalette.themePrimary } } },
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const textFieldStylesTaskName: ITextFieldStyles = {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const modalStyles: IModalStyles = {
|
||||
main: { minWidth: 400, maxWidth: 450 },
|
||||
root: {},
|
||||
keyboardMoveIcon: {},
|
||||
keyboardMoveIconContainer: {},
|
||||
layer: {},
|
||||
scrollableContent: {}
|
||||
};
|
||||
|
||||
export const datePickerStartDateStyles: IDatePickerStyles = {
|
||||
callout: {},
|
||||
icon: {},
|
||||
root: { marginTop: 0 },
|
||||
textField: { backgroundColor: '#f4f4f4', borderWidth: 0 }
|
||||
};
|
||||
|
||||
export const textFieldStylesdatePicker: ITextFieldProps = {
|
||||
style: { display: 'flex', justifyContent: 'flex-start', marginLeft: 15 },
|
||||
iconProps: { style: { left: 0 } }
|
||||
};
|
||||
|
||||
export const peoplePicker: IStyle = {
|
||||
backgroundColor: DefaultPalette.neutralLighter
|
||||
};
|
||||
|
||||
export const addMemberButton: IButtonStyles = {
|
||||
root: { marginLeft: 0, paddingLeft: 0, marginTop: 0, fontSize: FontSizes.medium },
|
||||
textContainer: {
|
||||
fontSize: FontSizes.medium,
|
||||
fontWeight: 'normal',
|
||||
color: '#666666',
|
||||
marginLeft: 5
|
||||
}
|
||||
};
|
||||
|
||||
export const dropDownBucketStyles: IDropdownStyles = {
|
||||
root: { margin: 0 },
|
||||
title: { backgroundColor: '#f4f4f4', borderWidth: 0 },
|
||||
callout: {},
|
||||
caretDown: {},
|
||||
caretDownWrapper: {},
|
||||
dropdown: {},
|
||||
dropdownDivider: {},
|
||||
dropdownItem: {},
|
||||
dropdownItemDisabled: {},
|
||||
dropdownItemHeader: {},
|
||||
dropdownItemHidden: {},
|
||||
dropdownItemSelected: {},
|
||||
dropdownItemSelectedAndDisabled: {},
|
||||
dropdownItems: {},
|
||||
dropdownItemsWrapper: {},
|
||||
dropdownOptionText: {},
|
||||
errorMessage: {},
|
||||
label: {},
|
||||
panel: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const dropDownProgressStyles: IDropdownStyles = {
|
||||
root: { margin: 0 },
|
||||
title: { backgroundColor: '#f4f4f4', borderWidth: 0 },
|
||||
callout: {},
|
||||
caretDown: {},
|
||||
caretDownWrapper: {},
|
||||
dropdown: {},
|
||||
dropdownDivider: {},
|
||||
dropdownItem: {},
|
||||
dropdownItemDisabled: {},
|
||||
dropdownItemHeader: {},
|
||||
dropdownItemHidden: {},
|
||||
dropdownItemSelected: {},
|
||||
dropdownItemSelectedAndDisabled: {},
|
||||
dropdownItems: {},
|
||||
dropdownItemsWrapper: {},
|
||||
dropdownOptionText: {},
|
||||
errorMessage: {},
|
||||
label: {},
|
||||
panel: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const chromeCloseButtomStyle: IIconStyles = {
|
||||
root: {
|
||||
fontSize: FontSizes.smallPlus
|
||||
}
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './UploadFile';
|
|
@ -0,0 +1,6 @@
|
|||
export interface IListViewItems {
|
||||
FileLeafRef: string;
|
||||
Modified: string;
|
||||
fileTypeImageUrl?: string;
|
||||
fileUrl:string;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import spservice from '../../services/spservices';
|
||||
import { IFile } from '../../webparts/myTasks/components/Attachments/IFile';
|
||||
import { ITaskExternalReference } from '../../services/ITaskExternalReference';
|
||||
export interface IUploadFromSharePointProps {
|
||||
spservice: spservice;
|
||||
onSelectedFile?: (file:IFile) => void;
|
||||
groupId:string;
|
||||
displayDialog: boolean;
|
||||
onDismiss: () => void;
|
||||
currentReferences: ITaskExternalReference;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IListViewItems } from "./../UploadFromSharePoint/IListViewItems";
|
||||
import { IColumn } from "office-ui-fabric-react";
|
||||
|
||||
export interface IUploadFromSharePointState {
|
||||
selectItem: IListViewItems;
|
||||
hasError: boolean;
|
||||
messageError:string;
|
||||
isloading: boolean;
|
||||
hideDialog: boolean;
|
||||
listViewItems: IListViewItems[];
|
||||
hasMoreItems:boolean;
|
||||
disableSaveButton:boolean;
|
||||
columns: IColumn[];
|
||||
messageInfo:string;
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
import * as React from 'react';
|
||||
import * as tsStyles from './UploadStyles';
|
||||
import InfiniteScroll from 'react-infinite-scroller';
|
||||
import styles from './UplaodFromSharePoint.module.scss';
|
||||
import {
|
||||
Callout,
|
||||
DefaultButton,
|
||||
DefaultPalette,
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
Dialog,
|
||||
DialogFooter,
|
||||
DialogType,
|
||||
FontWeights,
|
||||
IColumn,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconType,
|
||||
IFacepilePersona,
|
||||
IPersonaSharedProps,
|
||||
Label,
|
||||
mergeStyleSets,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Persona,
|
||||
PersonaBase,
|
||||
PersonaSize,
|
||||
PrimaryButton,
|
||||
Selection,
|
||||
SelectionMode,
|
||||
ShimmeredDetailsList,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Stack,
|
||||
TextField,
|
||||
TooltipHost,
|
||||
BaseButton,
|
||||
Button
|
||||
} from 'office-ui-fabric-react';
|
||||
import { format, parse, parseISO } from 'date-fns';
|
||||
import {
|
||||
GroupOrder,
|
||||
IGrouping,
|
||||
IViewField,
|
||||
ListView
|
||||
} from '@pnp/spfx-controls-react/lib/ListView';
|
||||
import { IListViewItems } from './IListViewItems';
|
||||
import { IUploadFromSharePointProps } from './IUploadFromSharePointProps';
|
||||
import { IUploadFromSharePointState } from './IUploadFromSharePointState';
|
||||
import { PagedItemCollection } from '@pnp/sp';
|
||||
import { utilities } from '../../utilities';
|
||||
import { ITaskExternalReference } from '../../services/ITaskExternalReference';
|
||||
import * as strings from 'MyTasksWebPartStrings';
|
||||
|
||||
export class UploadFromSharePoint extends React.Component<IUploadFromSharePointProps, IUploadFromSharePointState> {
|
||||
private _listItems: PagedItemCollection<any[]> ;
|
||||
private _selection: Selection;
|
||||
private util = new utilities();
|
||||
private _selectedItem : IListViewItems;
|
||||
constructor(props: IUploadFromSharePointProps) {
|
||||
super(props);
|
||||
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: 'column1',
|
||||
name: 'File_x0020_Type',
|
||||
className: tsStyles.classNames.fileIconCell,
|
||||
iconClassName: tsStyles.classNames.fileIconHeaderIcon,
|
||||
iconName: 'Page',
|
||||
isIconOnly: true,
|
||||
fieldName: 'name',
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IListViewItems) => {
|
||||
return <Icon iconType={IconType.Image} imageProps={{src: item.fileTypeImageUrl, height: 22, width: 22}} />;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Name',
|
||||
key: 'column2',
|
||||
fieldName: 'FileLeafRef',
|
||||
minWidth: 200,
|
||||
maxWidth: 250,
|
||||
isResizable: true,
|
||||
isSorted: false,
|
||||
isSortedDescending: false,
|
||||
sortAscendingAriaLabel: 'Sorted A to Z',
|
||||
sortDescendingAriaLabel: 'Sorted Z to A',
|
||||
onColumnClick: this._onColumnClick,
|
||||
data: 'string',
|
||||
isPadded: true
|
||||
},
|
||||
{
|
||||
key: 'column3',
|
||||
name: 'Date Modified',
|
||||
fieldName: 'Modified',
|
||||
minWidth: 70,
|
||||
maxWidth: 90,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
onColumnClick: this._onColumnClick,
|
||||
data: 'string',
|
||||
isPadded: true
|
||||
}
|
||||
];
|
||||
|
||||
this.state = {
|
||||
selectItem: undefined,
|
||||
hasError: false,
|
||||
messageError: undefined,
|
||||
isloading: true,
|
||||
hideDialog: !this.props.displayDialog,
|
||||
listViewItems: [],
|
||||
hasMoreItems: false,
|
||||
disableSaveButton: true,
|
||||
columns: columns,
|
||||
messageInfo:'',
|
||||
};
|
||||
|
||||
this._selection = new Selection({
|
||||
onSelectionChanged: () => {
|
||||
this._getSelectionDetails();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns upload from share point
|
||||
* @returns did mount
|
||||
*/
|
||||
public async componentDidMount(): Promise<void> {
|
||||
this.setState({isloading:true});
|
||||
await this._getListItems('Modified', false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Components did update
|
||||
* @param prevProps
|
||||
* @param prevState
|
||||
* @returns did update
|
||||
*/
|
||||
public async componentDidUpdate(prevProps: IUploadFromSharePointProps, prevState: IUploadFromSharePointState): Promise<void> {
|
||||
|
||||
if (this.props.groupId !== prevProps.groupId || this.props.displayDialog !== prevProps.displayDialog) {
|
||||
this.setState({ hideDialog: !this.props.displayDialog });
|
||||
this.setState({isloading:true});
|
||||
await this._getListItems('Modified', false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get list items of upload from share point
|
||||
*/
|
||||
private _getListItems = async (sortField: string , ascending:boolean) => {
|
||||
const { spservice, groupId } = this.props;
|
||||
const items: IListViewItems[] = [];
|
||||
|
||||
try {
|
||||
this._listItems = await spservice.getSharePointFiles(groupId,sortField, ascending);
|
||||
for (const item of this._listItems.results) {
|
||||
|
||||
if (item.File === undefined) continue;
|
||||
const fileTypeImageUrl = await this.util.GetFileImageUrl(item.File.Name);
|
||||
items.push({ FileLeafRef : item.File.Name, Modified: format(parseISO(item.File.TimeLastModified),'P'), fileTypeImageUrl: fileTypeImageUrl , fileUrl: item.File.ServerRelativeUrl});
|
||||
}
|
||||
this.setState({ listViewItems: items, hasError: false, messageError: '' , isloading: false, hasMoreItems: this._listItems.hasNext});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, messageError: error.message ,isloading: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list items next page of upload from share point
|
||||
*/
|
||||
private _getListItemsNextPage = async () => {
|
||||
this._listItems = await this._listItems.getNext();
|
||||
let items: IListViewItems[]=[];
|
||||
let {listViewItems} = this.state;
|
||||
for (const item of this._listItems.results) {
|
||||
console.log(item.File);
|
||||
if (item.File === undefined) continue;
|
||||
const fileTypeImageUrl = await this.util.GetFileImageUrl(item.File.Name);
|
||||
items.push({ FileLeafRef : item.File.Name, Modified: format(parseISO(item.File.TimeLastModified),'P'), fileTypeImageUrl: fileTypeImageUrl , fileUrl: item.File.ServerRelativeUrl});
|
||||
}
|
||||
this.setState({ listViewItems: listViewItems.concat(items), hasError: false, messageError: '' , isloading: false, hasMoreItems: this._listItems.hasNext});
|
||||
}
|
||||
/**
|
||||
* Close dialog of upload from share point
|
||||
*/
|
||||
private _closeDialog = (ev?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
this.setState({ hideDialog: true });
|
||||
this.props.onDismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether column click on
|
||||
*/
|
||||
private _onColumnClick = async (ev: React.MouseEvent<HTMLElement>, column: IColumn): Promise<void> => {
|
||||
// tslint:disable-next-line: no-shadowed-variable
|
||||
const { columns , hasMoreItems} = this.state;
|
||||
|
||||
const newColumns: IColumn[] = columns.slice();
|
||||
const currColumn: IColumn = newColumns.filter(currCol => column.key === currCol.key)[0];
|
||||
newColumns.forEach((newCol: IColumn) => {
|
||||
if (newCol === currColumn) {
|
||||
currColumn.isSortedDescending = !currColumn.isSortedDescending;
|
||||
currColumn.isSorted = true;
|
||||
|
||||
} else {
|
||||
newCol.isSorted = false;
|
||||
newCol.isSortedDescending = true;
|
||||
}
|
||||
});
|
||||
if ( hasMoreItems) { // has more items to load get items sorted by clicked columns and direction
|
||||
await this._getListItems(currColumn.fieldName, currColumn.isSortedDescending);
|
||||
}
|
||||
let {listViewItems} = this.state;
|
||||
// Sort Items
|
||||
const newItems = this._copyAndSort(listViewItems , currColumn.fieldName!, currColumn.isSortedDescending);
|
||||
this.setState({
|
||||
columns: newColumns,
|
||||
listViewItems : newItems
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copys and sort
|
||||
* @template T
|
||||
* @param items
|
||||
* @param columnKey
|
||||
* @param [isSortedDescending]
|
||||
* @returns and sort
|
||||
*/
|
||||
private _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
|
||||
const key = columnKey as keyof T;
|
||||
return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Gets selection details
|
||||
*/
|
||||
private _getSelectionDetails() {
|
||||
const selectionCount = this._selection.getSelectedCount();
|
||||
|
||||
switch (selectionCount) {
|
||||
case 0:
|
||||
this.setState({
|
||||
selectItem: null,
|
||||
disableSaveButton: true,
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
const currentReferences = this.props.currentReferences;
|
||||
const fileServerRelativeUrl = (this._selection.getSelection()[0] as IListViewItems).fileUrl;
|
||||
const fileFullUrl: string =
|
||||
(`${location.origin}${fileServerRelativeUrl}`).replace(/\./g, '%2E').replace(/\:/g, '%3A') + `?web=1`;
|
||||
if ( currentReferences[fileFullUrl] == null) {
|
||||
this.setState({
|
||||
selectItem: this._selection.getSelection()[0] as IListViewItems,
|
||||
disableSaveButton: false,
|
||||
messageInfo: '',
|
||||
});
|
||||
}else{
|
||||
this.setState({messageInfo: strings.FileAlreadyAddedToTaskLabel});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether select file on
|
||||
*/
|
||||
private _onSelectFile = async (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement | HTMLDivElement | BaseButton | Button, MouseEvent>) => {
|
||||
try {
|
||||
this.props.onSelectedFile({fileUrl: this.state.selectItem.fileUrl,FileLeafRef: this.state.selectItem.FileLeafRef});
|
||||
this.setState({ hideDialog: true });
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private _onActiveItemChanged = (item:IListViewItems,index:number, ev:React.FocusEvent<HTMLElement>) => {
|
||||
|
||||
ev.preventDefault();
|
||||
this._selectedItem = item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders upload from sharepoint
|
||||
* @returns render
|
||||
*/
|
||||
public render(): React.ReactElement<IUploadFromSharePointProps> {
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
hidden={this.state.hideDialog}
|
||||
onDismiss={this._closeDialog}
|
||||
minWidth={650}
|
||||
maxWidth={650}
|
||||
dialogContentProps={{
|
||||
type: DialogType.normal,
|
||||
title: strings.DocumentsLabel
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
styles: tsStyles.modalStyles
|
||||
// topOffsetFixed: true
|
||||
}}>
|
||||
{this.state.isloading ? (
|
||||
<div style={{ height: 300, overflow: 'auto' }}>
|
||||
<Spinner size={SpinnerSize.medium} label={strings.LoadingLabel}></Spinner>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{this.state.hasError && <MessageBar messageBarType={MessageBarType.error}>{this.state.messageError}</MessageBar>}
|
||||
<div style={{ height: 300, overflow: 'auto' }}>
|
||||
<Label>{this.state.messageInfo}</Label>
|
||||
<InfiniteScroll
|
||||
pageStart={0}
|
||||
loadMore={this._getListItemsNextPage}
|
||||
hasMore={this.state.hasMoreItems}
|
||||
threshold={20}
|
||||
useWindow={false}>
|
||||
<DetailsList
|
||||
items={this.state.listViewItems}
|
||||
compact={false}
|
||||
columns={this.state.columns}
|
||||
selectionMode={SelectionMode.single}
|
||||
setKey="none"
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
isHeaderVisible={true}
|
||||
selection={this._selection}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<PrimaryButton onClick={this._onSelectFile} text={strings.SaveLabel} disabled={this.state.disableSaveButton}/>
|
||||
<DefaultButton onClick={this._closeDialog} text={strings.CancelLabel} />
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
import { FontSizes, FontWeights, DefaultPalette, mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { CommunicationColors } from '@uifabric/fluent-theme/lib/fluent/FluentColors';
|
||||
import {
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
IStackItemStyles,
|
||||
ITextFieldStyles,
|
||||
ITextFieldSubComponentStyles,
|
||||
IModalStyles,
|
||||
ImageLoadState,
|
||||
IDatePickerStyles,
|
||||
ITextFieldProps,
|
||||
IStyle,
|
||||
IButtonStyles,
|
||||
calculatePrecision,
|
||||
IDropdownStyles,
|
||||
} from 'office-ui-fabric-react';
|
||||
|
||||
// Styles definition
|
||||
export const stackStyles: IStackStyles = {
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
marginTop: 10
|
||||
}
|
||||
};
|
||||
export const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
padding: 5,
|
||||
display: 'flex',
|
||||
width: 172,
|
||||
height: 32,
|
||||
fontWeight: FontWeights.regular,
|
||||
}
|
||||
};
|
||||
|
||||
export const stackTokens: IStackTokens = {
|
||||
childrenGap: 10,
|
||||
|
||||
};
|
||||
|
||||
export const textFielStartDateDatePickerStyles: ITextFieldProps = {
|
||||
styles: {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const textFielDueDateDatePickerStyles: ITextFieldProps = {
|
||||
styles: {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const textFieldDescriptionStyles: ITextFieldStyles = {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const textFieldCheckListItem: ITextFieldStyles = {
|
||||
field: { selectors:{ [':hover']: { backgroundColor: DefaultPalette.neutralLighter}}},
|
||||
root: { width:550,},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: { backgroundColor: 'white'},
|
||||
suffix: { backgroundColor: 'white'},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined,
|
||||
|
||||
};
|
||||
|
||||
export const textFieldStylesTaskName: ITextFieldStyles = {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {selectors:{ [':hover']: { borderWidth: 1,borderStyle:'solid', borderColor: DefaultPalette.themePrimary}}},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const modalStyles: IModalStyles = {
|
||||
main: { minWidth: 400 ,maxWidth: 450, },
|
||||
root: {},
|
||||
keyboardMoveIcon: {},
|
||||
keyboardMoveIconContainer: {},
|
||||
layer: {},
|
||||
scrollableContent: {}
|
||||
};
|
||||
|
||||
export const datePickerStartDateStyles: IDatePickerStyles = {
|
||||
callout: {},
|
||||
icon: {},
|
||||
root: { marginTop:0},
|
||||
textField: { backgroundColor: '#f4f4f4', borderWidth:0}
|
||||
};
|
||||
|
||||
export const textFieldStylesdatePicker: ITextFieldProps = {
|
||||
style: { display: 'flex', justifyContent: 'flex-start', marginLeft: 15 },
|
||||
iconProps: { style: { left: 0 } }
|
||||
};
|
||||
|
||||
export const peoplePicker: IStyle = {
|
||||
backgroundColor: DefaultPalette.neutralLighter
|
||||
};
|
||||
|
||||
export const addMemberButton: IButtonStyles = {
|
||||
root: { marginLeft: 0, paddingLeft: 0, marginTop: 0, fontSize: FontSizes.medium,width:26 },
|
||||
textContainer: {
|
||||
fontSize: FontSizes.medium,
|
||||
fontWeight: 'normal',
|
||||
color: '#666666',
|
||||
marginLeft: 5
|
||||
}
|
||||
};
|
||||
|
||||
export const dropDownBucketStyles: IDropdownStyles = {
|
||||
|
||||
root: { margin: 0 } ,
|
||||
title: {backgroundColor: '#f4f4f4', borderWidth:0},
|
||||
callout: {},
|
||||
caretDown: {},
|
||||
caretDownWrapper: {},
|
||||
dropdown:{},
|
||||
dropdownDivider: {},
|
||||
dropdownItem: {},
|
||||
dropdownItemDisabled: {},
|
||||
dropdownItemHeader:{},
|
||||
dropdownItemHidden: {},
|
||||
dropdownItemSelected:{},
|
||||
dropdownItemSelectedAndDisabled:{},
|
||||
dropdownItems:{},
|
||||
dropdownItemsWrapper:{},
|
||||
dropdownOptionText:{},
|
||||
errorMessage:{},
|
||||
label:{},
|
||||
panel:{},
|
||||
subComponentStyles: undefined,
|
||||
};
|
||||
|
||||
export const dropDownProgressStyles: IDropdownStyles = {
|
||||
|
||||
root: { margin: 0 } ,
|
||||
title: {backgroundColor: '#f4f4f4', borderWidth:0},
|
||||
callout: {},
|
||||
caretDown: {},
|
||||
caretDownWrapper: {},
|
||||
dropdown:{},
|
||||
dropdownDivider: {},
|
||||
dropdownItem: {},
|
||||
dropdownItemDisabled: {},
|
||||
dropdownItemHeader:{},
|
||||
dropdownItemHidden: {},
|
||||
dropdownItemSelected:{},
|
||||
dropdownItemSelectedAndDisabled:{},
|
||||
dropdownItems:{},
|
||||
dropdownItemsWrapper:{},
|
||||
dropdownOptionText:{},
|
||||
errorMessage:{},
|
||||
label:{},
|
||||
panel:{},
|
||||
subComponentStyles: undefined,
|
||||
};
|
||||
|
||||
|
||||
export const classNames = mergeStyleSets({
|
||||
fileIconHeaderIcon: {
|
||||
padding: 0,
|
||||
fontSize: '16px'
|
||||
},
|
||||
fileIconCell: {
|
||||
textAlign: 'center',
|
||||
selectors: {
|
||||
'&:before': {
|
||||
content: '.',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
height: '100%',
|
||||
width: '0px',
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
export * from './IUploadFromSharePointProps';
|
||||
export * from './IUploadFromSharePointState';
|
||||
export * from './UploadFromSharePoint';
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,3 @@
|
|||
export interface IAppliedCategories {
|
||||
[key:string]: boolean;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import {IPlannerAssignment} from './IPlannerAssignment';
|
||||
/**
|
||||
* Assignments
|
||||
*/
|
||||
export interface IAssignments {
|
||||
[key:string] : IPlannerAssignment;
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface IBucket {
|
||||
'@odata.etag': string;
|
||||
name: string;
|
||||
planId: string;
|
||||
orderHint: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export interface ICheckListItem {
|
||||
key: string;
|
||||
isChecked: boolean;
|
||||
lastModifiedBy?: string;
|
||||
lastModifiedByDateTime?: string;
|
||||
orderHint?: string;
|
||||
title: string;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*export interface IGroupMember {
|
||||
'@odata.type': string;
|
||||
id: string;
|
||||
businessPhones: string[];
|
||||
displayName: string;
|
||||
givenName: string;
|
||||
jobTitle: string;
|
||||
mail: string;
|
||||
mobilePhone: string;
|
||||
officeLocation?: any;
|
||||
preferredLanguage: string;
|
||||
surname: string;
|
||||
userPrincipalName: string;
|
||||
}*/
|
||||
|
||||
export interface IGroupMember {
|
||||
'@odata.context': string;
|
||||
'@odata.nextLink': string;
|
||||
value: IMember[];
|
||||
}
|
||||
|
||||
export interface IMember {
|
||||
'@odata.type': string;
|
||||
id: string;
|
||||
businessPhones: string[];
|
||||
displayName: string;
|
||||
givenName: string;
|
||||
jobTitle: string;
|
||||
mail: string;
|
||||
mobilePhone: string;
|
||||
officeLocation?: any;
|
||||
preferredLanguage: string;
|
||||
surname: string;
|
||||
userPrincipalName: string;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
export interface IGroup {
|
||||
'@odata.type': string;
|
||||
id: string;
|
||||
deletedDateTime?: any;
|
||||
classification?: any;
|
||||
createdDateTime: string;
|
||||
creationOptions: string[];
|
||||
description: string;
|
||||
displayName: string;
|
||||
groupTypes: string[];
|
||||
isAssignableToRole?: any;
|
||||
mail: string;
|
||||
mailEnabled: boolean;
|
||||
mailNickname: string;
|
||||
onPremisesLastSyncDateTime?: string;
|
||||
onPremisesSecurityIdentifier?: string;
|
||||
onPremisesSyncEnabled?: boolean;
|
||||
preferredDataLocation?: any;
|
||||
proxyAddresses: string[];
|
||||
renewedDateTime: string;
|
||||
resourceBehaviorOptions: any[];
|
||||
resourceProvisioningOptions: string[];
|
||||
securityEnabled: boolean;
|
||||
visibility: string;
|
||||
onPremisesProvisioningErrors: any[];
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import {IUser} from './IUser';
|
||||
export interface IIdentitySet {
|
||||
application: Application;
|
||||
device: Application;
|
||||
user: IUser;
|
||||
}
|
||||
|
||||
interface Application {
|
||||
'@odata.type': string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import {IUser} from './IUser';
|
||||
export interface IPlannerAssignment {
|
||||
"@odata.type"?:string;
|
||||
assignedDateTime?: string;
|
||||
orderHint: string;
|
||||
assignedBy?: IUser;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface IPlannerBucket {
|
||||
'@odata.etag': string;
|
||||
name: string;
|
||||
planId: string;
|
||||
orderHint: string;
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { IUser } from "./IUser";
|
||||
|
||||
|
||||
export interface IPlannerPlan {
|
||||
createdBy?: IUser;
|
||||
createdDateTime: string;
|
||||
id: string;
|
||||
owner: string;
|
||||
title: string;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export interface IPlannerPlanDetails{
|
||||
categoryDescriptions: {[key:string]: string};
|
||||
id: string;
|
||||
sharedWith: {[key:string]: boolean};
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { IPlannerPlan} from './IPlannerPlan';
|
||||
export interface IPlannerPlanExtended extends IPlannerPlan {
|
||||
planPhoto?: string;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { IAppliedCategories } from './IAppliedCategories';
|
||||
import { IAssignments } from './IAssignments';
|
||||
import { IUser } from './IUser';
|
||||
import { IIdentitySet} from './IIdentitySet';
|
||||
|
||||
export interface ITask {
|
||||
'@odata.context': string;
|
||||
'@odata.etag': string;
|
||||
planId: string;
|
||||
bucketId: string;
|
||||
title: string;
|
||||
orderHint: string;
|
||||
assigneePriority: string;
|
||||
percentComplete: number;
|
||||
startDateTime: string;
|
||||
createdDateTime: string;
|
||||
dueDateTime: string;
|
||||
hasDescription: boolean;
|
||||
previewType: string;
|
||||
completedDateTime?: string;
|
||||
completedBy?: IIdentitySet;
|
||||
referenceCount: number;
|
||||
checklistItemCount: number;
|
||||
activeChecklistItemCount: number;
|
||||
conversationThreadId?: string;
|
||||
id: string;
|
||||
createdBy: IUser;
|
||||
appliedCategories: IAppliedCategories;
|
||||
assignments: IAssignments;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export interface ITaskCheckListItem {
|
||||
[key:string]: {
|
||||
"@odata.type":string;
|
||||
isChecked: boolean;
|
||||
lastModifiedBy?: string;
|
||||
lastModifiedByDateTime?: string;
|
||||
orderHint: string;
|
||||
title: string ; };
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { ITaskCheckListItem} from './ITaskCheckListItem';
|
||||
import { ITaskExternalReference } from './ITaskExternalReference';
|
||||
export interface ITaskDetails {
|
||||
"@odata.context":string;
|
||||
"@odata.etag":string;
|
||||
checklist: ITaskCheckListItem[];
|
||||
description: string;
|
||||
id: string;
|
||||
previewType: string;
|
||||
references: ITaskExternalReference;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export interface ITaskExternalReference {
|
||||
[key:string]: Stringvalue;
|
||||
}
|
||||
|
||||
interface Stringvalue {
|
||||
"@odata.type": string;
|
||||
alias: string;
|
||||
lastModifiedBy?: string;
|
||||
lastModifiedDateTime?: string;
|
||||
previewPriority?: string;
|
||||
type?: string;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface ITaskProperty {
|
||||
[property:string]: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface IUser {
|
||||
displayName?: any;
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,682 @@
|
|||
import * as moment from 'moment';
|
||||
import { Bucket, DirectoryObject, graph, Plan, Planner, TaskAddResult, Group } from '@pnp/graph';
|
||||
import { IGroup } from './IGroups';
|
||||
import { IGroupMember, IMember } from './IGroupMembers';
|
||||
import { IPlannerBucket } from './IPlannerBucket';
|
||||
import { IPlannerPlan } from './IPlannerPlan';
|
||||
import { IPlannerPlanDetails } from './IPlannerPlanDetails';
|
||||
import { IPlannerPlanExtended } from './IPlannerPlanExtended';
|
||||
import { ITask } from './ITask';
|
||||
import { ITaskDetails } from './ITaskDetails';
|
||||
import { ITaskProperty } from './ITaskProperty';
|
||||
import { IUser } from './IUser';
|
||||
import { MSGraphClient, SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { PropertyPaneDynamicFieldSet, WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import {
|
||||
SearchProperty,
|
||||
SearchQuery,
|
||||
SearchResults,
|
||||
SortDirection,
|
||||
sp,
|
||||
Web,
|
||||
PagedItemCollection,
|
||||
ChunkedFileUploadProgressData,
|
||||
FileAddResult
|
||||
} from '@pnp/pnpjs';
|
||||
import { SPComponentLoader } from '@microsoft/sp-loader';
|
||||
import { User } from '@pnp/graph/src/users';
|
||||
import * as $ from 'jquery';
|
||||
import { List } from 'office-ui-fabric-react';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
|
||||
const DEFAULT_PERSONA_IMG_HASH: string = '7ad602295f8386b7615b582d87bcc294';
|
||||
const DEFAULT_IMAGE_PLACEHOLDER_HASH: string = '4a48f26592f4e1498d7a478a4c48609c';
|
||||
const MD5_MODULE_ID: string = '8494e7d7-6b99-47b2-a741-59873e42f16f';
|
||||
const PROFILE_IMAGE_URL: string = '/_layouts/15/userphoto.aspx?size=M&accountname=';
|
||||
|
||||
export default class spservices {
|
||||
private graphClient: MSGraphClient = null;
|
||||
public currentUser: string = undefined;
|
||||
|
||||
constructor(public context: WebPartContext) {
|
||||
/*
|
||||
Initialize MSGraph
|
||||
*/
|
||||
|
||||
sp.setup({
|
||||
spfxContext: this.context
|
||||
});
|
||||
graph.setup({
|
||||
spfxContext: this.context
|
||||
});
|
||||
|
||||
this.currentUser = this.context.pageContext.user.email;
|
||||
}
|
||||
|
||||
public async getTaskById(taskId: string): Promise<ITask> {
|
||||
try {
|
||||
const task: ITask = await graph.planner.tasks.getById(taskId).get();
|
||||
return task;
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user groups
|
||||
* @returns user groups
|
||||
*/
|
||||
public async getUserGroups(): Promise<IGroup[]> {
|
||||
let o365Groups: IGroup[] = [];
|
||||
try {
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const groups = await this.graphClient
|
||||
.api(`me/memberof`)
|
||||
.version('v1.0')
|
||||
.get();
|
||||
// Get O365 Groups
|
||||
for (const group of groups.value as IGroup[]) {
|
||||
const hasO365Group = group.groupTypes && group.groupTypes.length > 0 ? group.groupTypes.indexOf('Unified') : -1;
|
||||
if (hasO365Group !== -1) {
|
||||
o365Groups.push(group);
|
||||
}
|
||||
}
|
||||
return o365Groups;
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user plans by group id
|
||||
* @param groupId
|
||||
* @returns user plans by group id
|
||||
*/
|
||||
public async getUserPlansByGroupId(groupId: string): Promise<IPlannerPlan[]> {
|
||||
try {
|
||||
const groupPlans: IPlannerPlan[] = await graph.groups.getById(groupId).plans.get();
|
||||
return groupPlans;
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user plans
|
||||
* @returns user plans
|
||||
*/
|
||||
public async getUserPlans(): Promise<IPlannerPlanExtended[]> {
|
||||
try {
|
||||
let userPlans: IPlannerPlanExtended[] = [];
|
||||
const o365Groups: IGroup[] = await this.getUserGroups();
|
||||
for (const group of o365Groups) {
|
||||
const plans: IPlannerPlan[] = await this.getUserPlansByGroupId(group.id);
|
||||
for (const plan of plans) {
|
||||
const groupPhoto: string = await this.getGroupPhoto(group.id);
|
||||
const userPlan: IPlannerPlanExtended = { ...plan, planPhoto: groupPhoto };
|
||||
userPlans.push(userPlan);
|
||||
}
|
||||
}
|
||||
// Sort plans by Title
|
||||
userPlans = userPlans.sort((a, b) => {
|
||||
if (a.title < b.title) return -1;
|
||||
if (a.title > b.title) return 1;
|
||||
return 0;
|
||||
});
|
||||
return userPlans;
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets group photo
|
||||
* @param groupId
|
||||
* @returns group photo
|
||||
*/
|
||||
public async getGroupPhoto(groupId: string): Promise<any> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let url: any = '';
|
||||
const photo = await graph.groups.getById(groupId).photo.getBlob();
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
url = reader.result; // data url
|
||||
resolve(url);
|
||||
},
|
||||
false
|
||||
);
|
||||
reader.readAsDataURL(photo); // converts the blob to base64 and calls onload
|
||||
} catch (error) {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Gets tasks
|
||||
* @returns tasks
|
||||
*/
|
||||
public async getTasks(): Promise<ITask[]> {
|
||||
try {
|
||||
//const myTasks: ITask[] = await graph.me.tasks.get();
|
||||
let myTasks: ITask[] = [];
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const results: any = await this.graphClient
|
||||
.api(`/me/planner/tasks`)
|
||||
.version('v1.0')
|
||||
.get();
|
||||
|
||||
return results.value;
|
||||
} catch (error) {
|
||||
throw new Error('Error on get user task');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets group members
|
||||
* @param groupId
|
||||
* @returns group members
|
||||
*/
|
||||
public async getGroupMembers(groupId: string, skipToken: string): Promise<IGroupMember> {
|
||||
try {
|
||||
let groupMembers: IGroupMember;
|
||||
if (skipToken) {
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
groupMembers = await this.graphClient
|
||||
.api(`groups/${groupId}/members`)
|
||||
.version('v1.0')
|
||||
.skipToken(skipToken)
|
||||
.get();
|
||||
} else {
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
groupMembers = await this.graphClient
|
||||
.api(`groups/${groupId}/members`)
|
||||
.version('v1.0')
|
||||
.top(100)
|
||||
.get();
|
||||
}
|
||||
return groupMembers;
|
||||
} catch (error) {
|
||||
throw new Error('Error on get group members');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets task details
|
||||
* @param taskId
|
||||
* @returns task details
|
||||
*/
|
||||
public async getTaskDetails(taskId: string): Promise<ITaskDetails> {
|
||||
try {
|
||||
const taskDetails: ITaskDetails = await graph.planner.tasks.getById(taskId).details.get();
|
||||
return taskDetails;
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates task as completed
|
||||
* @param taskId
|
||||
* @param etag
|
||||
* @returns task as completed
|
||||
*/
|
||||
public async updateTaskAsCompleted(taskId: string, etag: string): Promise<void> {
|
||||
try {
|
||||
// await graph.planner.tasks.getById(taskId).update({percentComplete: 100});
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
await this.graphClient
|
||||
.api(`planner/tasks/${taskId}`)
|
||||
.version('v1.0')
|
||||
.header(`If-Match`, etag)
|
||||
.patch({ percentComplete: 100 });
|
||||
} catch (error) {
|
||||
throw new Error('Error get task Details');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates task not started
|
||||
* @param taskId
|
||||
* @param etag
|
||||
* @returns task not started
|
||||
*/
|
||||
public async updateTaskNotStarted(taskId: string, etag: string): Promise<void> {
|
||||
try {
|
||||
//await graph.planner.tasks.getById(taskId).update({percentComplete: 100});
|
||||
await this.graphClient
|
||||
.api(`planner/tasks/${taskId}`)
|
||||
.version('v1.0')
|
||||
.header(`If-Match`, etag)
|
||||
.patch({ percentComplete: 0 });
|
||||
} catch (error) {
|
||||
throw new Error('Error on update task progress');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* async getPlan
|
||||
planId:string :Promise<string> */
|
||||
public async getPlan(planId: string): Promise<IPlannerPlan> {
|
||||
try {
|
||||
const plan: Plan = await graph.planner.plans.getById(planId);
|
||||
const plannerPlan: IPlannerPlan = await plan.get();
|
||||
|
||||
return plannerPlan;
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async updatePlannerDetailsProperty(
|
||||
plannerId: string,
|
||||
plannerDetailsPropertyName: string,
|
||||
plannerDetailsPropertyValue: any,
|
||||
etag: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
let parameterValue: any;
|
||||
// Test typeof parameter
|
||||
switch (typeof plannerDetailsPropertyValue) {
|
||||
case 'object':
|
||||
parameterValue = JSON.stringify(plannerDetailsPropertyValue);
|
||||
break;
|
||||
case 'number':
|
||||
parameterValue = plannerDetailsPropertyValue;
|
||||
break;
|
||||
case 'string':
|
||||
parameterValue = `'${plannerDetailsPropertyValue}'`;
|
||||
break;
|
||||
default:
|
||||
parameterValue = `'${plannerDetailsPropertyValue}'`;
|
||||
break;
|
||||
}
|
||||
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
await this.graphClient
|
||||
.api(`planner/plans/${plannerId}/details`)
|
||||
.version('v1.0')
|
||||
.header(`If-Match`, etag)
|
||||
.patch(`{${plannerDetailsPropertyName} : ${parameterValue}}`);
|
||||
|
||||
// const _task: ITaskProperty = {[taskPropertyName] : taskPropertyValue};
|
||||
// await graph.planner.tasks.getById(taskId).update(_task, etag);
|
||||
const plannerDetails: IPlannerPlanDetails = await this.getPlanDetails(plannerId);
|
||||
return plannerDetails['@odata.etag'];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error('Error on update planner Details');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds task
|
||||
* @param taskInfo
|
||||
* @returns task
|
||||
*/
|
||||
public async updateTaskProperty(taskId: string, taskPropertyName: string, taskPropertyValue: any, etag: string): Promise<ITask> {
|
||||
try {
|
||||
let parameterValue: any;
|
||||
// Test typeof parameter
|
||||
switch (typeof taskPropertyValue) {
|
||||
case 'object':
|
||||
parameterValue = JSON.stringify(taskPropertyValue);
|
||||
break;
|
||||
case 'number':
|
||||
parameterValue = taskPropertyValue;
|
||||
break;
|
||||
case 'string':
|
||||
parameterValue = `'${taskPropertyValue}'`;
|
||||
break;
|
||||
default:
|
||||
parameterValue = `'${taskPropertyValue}'`;
|
||||
break;
|
||||
}
|
||||
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const task = await this.graphClient
|
||||
.api(`planner/tasks/${taskId}`)
|
||||
.version('v1.0')
|
||||
.headers({["Prefer"]: "return=representation", ["if-Match"]: etag, ["Content-type"]: "application/json"})
|
||||
.patch(`{${taskPropertyName} : ${parameterValue}}`);
|
||||
|
||||
// const _task: ITaskProperty = {[taskPropertyName] : taskPropertyValue};
|
||||
// await graph.planner.tasks.getById(taskId).update(_task, etag);
|
||||
// const task = await this.getTaskById(taskId);
|
||||
// return task2['@odata.etag'];
|
||||
return task;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error('Error on add task');
|
||||
}
|
||||
}
|
||||
|
||||
public async updateTaskDetailsProperty(
|
||||
taskId: string,
|
||||
taskPropertyName: string,
|
||||
taskPropertyValue: object | number | string,
|
||||
etag: string
|
||||
): Promise<ITaskDetails> {
|
||||
try {
|
||||
let parameterValue: any;
|
||||
// Test typeof parameter
|
||||
switch (typeof taskPropertyValue) {
|
||||
case 'object':
|
||||
parameterValue = JSON.stringify(taskPropertyValue);
|
||||
break;
|
||||
case 'number':
|
||||
parameterValue = taskPropertyValue;
|
||||
break;
|
||||
case 'string':
|
||||
parameterValue = `'${taskPropertyValue}'`;
|
||||
break;
|
||||
default:
|
||||
parameterValue = `'${taskPropertyValue}'`;
|
||||
break;
|
||||
}
|
||||
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const taskDetails = await this.graphClient
|
||||
.api(`planner/tasks/${taskId}/details`)
|
||||
.version('v1.0')
|
||||
.headers({["Prefer"]: "return=representation", ["if-Match"]: etag, ["Content-type"]: "application/json"})
|
||||
.patch(`{${taskPropertyName} : ${parameterValue}}`);
|
||||
|
||||
// const _task: ITaskProperty = {[taskPropertyName] : taskPropertyValue};
|
||||
// await graph.planner.tasks.getById(taskId).update(_task, etag);
|
||||
//const taskDetails = await this.getTaskDetails(taskId);
|
||||
// return taskDetails['@odata.etag'];
|
||||
return taskDetails;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error('Error on update property Task, please try later.');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds task
|
||||
* @param taskInfo
|
||||
* @returns task
|
||||
*/
|
||||
public async addTask(taskInfo: string[]): Promise<TaskAddResult> {
|
||||
try {
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const task = await this.graphClient
|
||||
.api(`planner/tasks`)
|
||||
.version('v1.0')
|
||||
.post({
|
||||
planId: taskInfo['planId'],
|
||||
bucketId: taskInfo['bucketId'],
|
||||
title: taskInfo['title'],
|
||||
dueDateTime: taskInfo['dueDate'] ? moment(taskInfo['dueDate']).toISOString() : undefined,
|
||||
assignments: taskInfo['assignments']
|
||||
});
|
||||
|
||||
//const task: TaskAddResult = await graph.planner.tasks.add( taskInfo['planId'], taskInfo['title'], taskInfo['assignments'], taskInfo['bucketId']);
|
||||
return task;
|
||||
} catch (error) {
|
||||
throw new Error('Error on add task');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteTask(taskId: string, etag:string): Promise<void> {
|
||||
try {
|
||||
// await graph.planner.tasks.getById(taskId).update({percentComplete: 100});
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
await this.graphClient
|
||||
.api(`planner/tasks/${taskId}`)
|
||||
.version('v1.0')
|
||||
.header(`If-Match`, etag)
|
||||
.delete();
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets plan details
|
||||
* @param planId
|
||||
* @returns plan details
|
||||
*/
|
||||
public async getPlanDetails(planId: string): Promise<IPlannerPlanDetails> {
|
||||
try {
|
||||
const plan: Plan = await graph.planner.plans.getById(planId);
|
||||
const plannerPlanDetails: IPlannerPlanDetails = await plan.details.get();
|
||||
await this.getPlanBuckets(planId);
|
||||
return plannerPlanDetails;
|
||||
} catch (error) {
|
||||
throw new Error('Error on get planner details');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets plan buckets
|
||||
* @param planId
|
||||
* @returns plan buckets
|
||||
*/
|
||||
public async getPlanBuckets(planId: string): Promise<IPlannerBucket[]> {
|
||||
try {
|
||||
const plan: Plan = await graph.planner.plans.getById(planId);
|
||||
const plannerBuckets: IPlannerBucket[] = await plan.buckets.get();
|
||||
return plannerBuckets;
|
||||
} catch (error) {
|
||||
throw new Error('Error get Planner buckets');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets user
|
||||
* @param userId
|
||||
* @returns user
|
||||
*/
|
||||
public async getUser(userId: string): Promise<IMember> {
|
||||
try {
|
||||
const user: IMember = await graph.users.getById(userId).get();
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new Error('Error on get user details');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public async getPhoto(userId: string): Promise<any> {
|
||||
let photo: any = undefined;
|
||||
try {
|
||||
let photoBlob = await graph.users.getById(userId).photo.getBlob();
|
||||
let groupPhotoUrl = window.URL;
|
||||
photo = groupPhotoUrl.createObjectURL(photoBlob);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
return photo;
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Gets user photo
|
||||
* @param userId
|
||||
* @returns user photo
|
||||
*/
|
||||
public async getUserPhoto(userId): Promise<string> {
|
||||
const personaImgUrl = PROFILE_IMAGE_URL + userId;
|
||||
const url: string = await this.getImageBase64(personaImgUrl);
|
||||
const newHash = await this.getMd5HashForUrl(url);
|
||||
|
||||
if (newHash !== DEFAULT_PERSONA_IMG_HASH && newHash !== DEFAULT_IMAGE_PLACEHOLDER_HASH) {
|
||||
return 'data:image/png;base64,' + url;
|
||||
} else {
|
||||
return 'undefined';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MD5Hash for the image url to verify whether user has default image or custom image
|
||||
* @param url
|
||||
*/
|
||||
private getMd5HashForUrl(url: string) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const library: any = await this.loadSPComponentById(MD5_MODULE_ID);
|
||||
try {
|
||||
const md5Hash = library.Md5Hash;
|
||||
if (md5Hash) {
|
||||
const convertedHash = md5Hash(url);
|
||||
resolve(convertedHash);
|
||||
}
|
||||
} catch (error) {
|
||||
resolve(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load SPFx component by id, SPComponentLoader is used to load the SPFx components
|
||||
* @param componentId - componentId, guid of the component library
|
||||
*/
|
||||
private loadSPComponentById(componentId: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
SPComponentLoader.loadComponentById(componentId)
|
||||
.then((component: any) => {
|
||||
resolve(component);
|
||||
})
|
||||
.catch(error => {});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Gets image base64
|
||||
* @param pictureUrl
|
||||
* @returns image base64
|
||||
*/
|
||||
private getImageBase64(pictureUrl: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let image = new Image();
|
||||
image.addEventListener('load', () => {
|
||||
let tempCanvas = document.createElement('canvas');
|
||||
(tempCanvas.width = image.width), (tempCanvas.height = image.height), tempCanvas.getContext('2d').drawImage(image, 0, 0);
|
||||
let base64Str;
|
||||
try {
|
||||
base64Str = tempCanvas.toDataURL('image/png');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
base64Str = base64Str.replace(/^data:image\/png;base64,/, '');
|
||||
resolve(base64Str);
|
||||
});
|
||||
image.src = pictureUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searchs users
|
||||
* @param searchString
|
||||
* @returns users
|
||||
*/
|
||||
public async searchUsers(searchString: string): Promise<IMember[]> {
|
||||
try {
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const returnUsers = await this.graphClient
|
||||
.api(`users`)
|
||||
.version('v1.0')
|
||||
.top(100)
|
||||
.filter(`startswith(DisplayName, '${searchString}') or startswith(mail, '${searchString}')`)
|
||||
.get();
|
||||
|
||||
return returnUsers.value;
|
||||
} catch (error) {
|
||||
throw new Error('Error on search users');
|
||||
}
|
||||
}
|
||||
|
||||
public async getGrouoUrl(groupId): Promise<string> {
|
||||
try {
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const returnGroupInfo = await this.graphClient
|
||||
.api(`groups/${groupId}/sites/root`)
|
||||
.version('v1.0')
|
||||
.get();
|
||||
|
||||
return returnGroupInfo.webUrl;
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
throw new Error('Error get group Url');
|
||||
}
|
||||
}
|
||||
|
||||
public async getSharePointFiles(groupId: string, sortField: string, ascending: boolean): Promise<PagedItemCollection<any[]>> {
|
||||
try {
|
||||
// let libraryUrl: string = await this.getGroupDocumentLibraryUrl(groupId);
|
||||
const groupUrl: string = await this.getGrouoUrl(groupId);
|
||||
|
||||
const web = new Web(groupUrl);
|
||||
const defualtDocumentLibrary = await web.defaultDocumentLibrary.get();
|
||||
const results: PagedItemCollection<any[]> = await web.lists
|
||||
.getById(defualtDocumentLibrary.Id)
|
||||
.items.select(
|
||||
'Title',
|
||||
'File_x0020_Type',
|
||||
'FileSystemObjectType',
|
||||
'File/Name',
|
||||
'File/ServerRelativeUrl',
|
||||
'File/Title',
|
||||
'File/Id',
|
||||
'File/TimeLastModified'
|
||||
)
|
||||
.top(8)
|
||||
.expand('File')
|
||||
.orderBy(`${sortField}`, ascending)
|
||||
.getPaged();
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw new Error('Error on read files from Documents ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets group document library url
|
||||
* @param groupId
|
||||
* @returns group document library url
|
||||
*/
|
||||
public async getGroupDocumentLibraryUrl(groupId: string): Promise<string> {
|
||||
try {
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
const returnGroupDriveInfo = await this.graphClient
|
||||
.api(`groups/${groupId}/sites/root/drive`)
|
||||
.version('v1.0')
|
||||
.get();
|
||||
|
||||
return returnGroupDriveInfo.webUrl;
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
throw new Error('Error get group default Document Library');
|
||||
}
|
||||
}
|
||||
|
||||
public ValidateEmail(mail: string): boolean {
|
||||
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(mail)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async uploadFileToSharePoint(documentLibrary: string, webUrl: string, fileName: string, fileB64: any): Promise<any> {
|
||||
try {
|
||||
const web = new Web(webUrl);
|
||||
documentLibrary = documentLibrary.replace(location.origin, '');
|
||||
console.log(documentLibrary);
|
||||
|
||||
const rs: FileAddResult = await web.getFolderByServerRelativeUrl(documentLibrary).files.addChunked(
|
||||
fileName,
|
||||
fileB64,
|
||||
(data: ChunkedFileUploadProgressData) => {
|
||||
console.log('File Upload chunked %', data.currentPointer / data.fileSize);
|
||||
return data.currentPointer / data.fileSize;
|
||||
},
|
||||
true
|
||||
);
|
||||
return rs;
|
||||
// const rs:FileAddResult = await web.getFolderByServerRelativeUrl(documentLibrary).files.add(fileName,fileB64,true);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './utilities';
|
|
@ -0,0 +1,208 @@
|
|||
export const DOCICONURL_XLSX = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/xlsx.png';
|
||||
export const DOCICONURL_DOCX = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/docx.png';
|
||||
export const DOCICONURL_PPTX = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/pptx.png';
|
||||
export const DOCICONURL_MPPX = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/mpp.png';
|
||||
export const DOCICONURL_PHOTO = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/photo.png';
|
||||
export const DOCICONURL_PDF = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/pdf.png';
|
||||
export const DOCICONURL_TXT = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/txt.png';
|
||||
export const DOCICONURL_EMAIL = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/email.png';
|
||||
export const DOCICONURL_CSV = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/csv.png';
|
||||
export const DOCICONURL_ONE = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/one.png';
|
||||
export const DOCICONURL_VSDX = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/vsdx.png';
|
||||
export const DOCICONURL_VSSX = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/vssx.png';
|
||||
export const DOCICONURL_PUB = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/pub.png';
|
||||
export const DOCICONURL_ACCDB = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/accdb.png';
|
||||
export const DOCICONURL_ZIP = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/zip.png';
|
||||
export const DOCICONURL_GENERIC = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/genericfile.png';
|
||||
export const DOCICONURL_CODE = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/code.png';
|
||||
export const DOCICONURL_HTML = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/html.png';
|
||||
export const DOCICONURL_XML = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/xml.png';
|
||||
export const DOCICONURL_SPO = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/spo.png';
|
||||
export const DOCICONURL_VIDEO = 'https://static2.sharepointonline.com/files/fabric/assets/item-types/96/video.png';
|
||||
const defaultLinkImg: string = require('./../../assets/mbri-link.svg');
|
||||
|
||||
export class utilities {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* GetFileImageUrl
|
||||
*/
|
||||
public GetFileImageUrl(_file: string): Promise<string> {
|
||||
|
||||
_file = _file.replace('?web=1','');
|
||||
let _fileImageUrl: string = defaultLinkImg;
|
||||
|
||||
try {
|
||||
const url = new URL(_file);
|
||||
_fileImageUrl = `${url.origin}/favicon.ico`;
|
||||
} catch (error) {
|
||||
_fileImageUrl= defaultLinkImg;
|
||||
}
|
||||
const _fileTypes = _file.split('.');
|
||||
const fileType = _fileTypes[_fileTypes.length - 1];
|
||||
if (!fileType) {
|
||||
return Promise.resolve(_fileImageUrl);
|
||||
}
|
||||
switch (fileType.toLowerCase()) {
|
||||
case 'xlsx':
|
||||
_fileImageUrl = DOCICONURL_XLSX;
|
||||
break;
|
||||
case 'xls':
|
||||
_fileImageUrl = DOCICONURL_XLSX;
|
||||
break;
|
||||
case 'docx':
|
||||
_fileImageUrl = DOCICONURL_DOCX;
|
||||
break;
|
||||
case 'doc':
|
||||
_fileImageUrl = DOCICONURL_DOCX;
|
||||
break;
|
||||
case 'pptx':
|
||||
_fileImageUrl = DOCICONURL_PPTX;
|
||||
break;
|
||||
case 'ppt':
|
||||
_fileImageUrl = DOCICONURL_PPTX;
|
||||
break;
|
||||
case 'mppx':
|
||||
_fileImageUrl = DOCICONURL_MPPX;
|
||||
break;
|
||||
case 'mpp':
|
||||
_fileImageUrl = DOCICONURL_MPPX;
|
||||
break;
|
||||
case 'csv':
|
||||
_fileImageUrl = DOCICONURL_CSV;
|
||||
break;
|
||||
case 'pdf':
|
||||
_fileImageUrl = DOCICONURL_PDF;
|
||||
break;
|
||||
case 'txt':
|
||||
_fileImageUrl = DOCICONURL_TXT;
|
||||
break;
|
||||
case 'jpg':
|
||||
_fileImageUrl = DOCICONURL_PHOTO;
|
||||
break;
|
||||
case 'msg':
|
||||
_fileImageUrl = DOCICONURL_EMAIL;
|
||||
break;
|
||||
case 'jpeg':
|
||||
_fileImageUrl = DOCICONURL_PHOTO;
|
||||
break;
|
||||
case 'png':
|
||||
_fileImageUrl = DOCICONURL_PHOTO;
|
||||
break;
|
||||
case 'ico':
|
||||
_fileImageUrl = DOCICONURL_PHOTO;
|
||||
break;
|
||||
case 'tiff':
|
||||
_fileImageUrl = DOCICONURL_PHOTO;
|
||||
break;
|
||||
case 'eml':
|
||||
_fileImageUrl = DOCICONURL_EMAIL;
|
||||
break;
|
||||
case 'pub':
|
||||
_fileImageUrl = DOCICONURL_PUB;
|
||||
break;
|
||||
case 'accdb':
|
||||
_fileImageUrl = DOCICONURL_ACCDB;
|
||||
break;
|
||||
case 'zip':
|
||||
_fileImageUrl = DOCICONURL_ZIP;
|
||||
break;
|
||||
case '7z':
|
||||
_fileImageUrl = DOCICONURL_ZIP;
|
||||
break;
|
||||
case 'tar':
|
||||
_fileImageUrl = DOCICONURL_ZIP;
|
||||
break;
|
||||
case 'js':
|
||||
_fileImageUrl = DOCICONURL_CODE;
|
||||
break;
|
||||
case 'html':
|
||||
_fileImageUrl = DOCICONURL_HTML;
|
||||
break;
|
||||
case 'xml':
|
||||
_fileImageUrl = DOCICONURL_XML;
|
||||
break;
|
||||
case 'aspx':
|
||||
_fileImageUrl = DOCICONURL_SPO;
|
||||
break;
|
||||
case 'mp4':
|
||||
_fileImageUrl = DOCICONURL_VIDEO;
|
||||
break;
|
||||
case 'mov':
|
||||
_fileImageUrl = DOCICONURL_VIDEO;
|
||||
break;
|
||||
case 'wmv':
|
||||
_fileImageUrl = DOCICONURL_VIDEO;
|
||||
break;
|
||||
case 'ogg':
|
||||
_fileImageUrl = DOCICONURL_VIDEO;
|
||||
break;
|
||||
case 'webm':
|
||||
_fileImageUrl = DOCICONURL_VIDEO;
|
||||
break;
|
||||
case 'pkg':
|
||||
_fileImageUrl = DOCICONURL_ZIP;
|
||||
break;
|
||||
default:
|
||||
|
||||
break;
|
||||
}
|
||||
return Promise.resolve(_fileImageUrl);
|
||||
}
|
||||
|
||||
public getFileType(_file: string): Promise<string> {
|
||||
let _fileType: string = 'Other';
|
||||
_file = _file.replace('?web=1','');
|
||||
const _fileTypes = _file.split('.');
|
||||
const fileType = _fileTypes[_fileTypes.length - 1];
|
||||
if (!fileType) {
|
||||
return Promise.resolve(_fileType);
|
||||
}
|
||||
switch (fileType.toLowerCase()) {
|
||||
case 'xlsx':
|
||||
_fileType = 'Excel';
|
||||
break;
|
||||
case 'xls':
|
||||
_fileType = 'Excel';
|
||||
break;
|
||||
case 'docx':
|
||||
_fileType = 'Word';
|
||||
break;
|
||||
case 'doc':
|
||||
_fileType = 'Word';
|
||||
break;
|
||||
case 'pptx':
|
||||
_fileType = 'PowerPoint';
|
||||
break;
|
||||
case 'ppt':
|
||||
_fileType = 'PowerPoint';
|
||||
break;
|
||||
case 'pdf':
|
||||
_fileType = 'Pdf';
|
||||
break;
|
||||
case 'one':
|
||||
_fileType = 'OneNote';
|
||||
break;
|
||||
case 'mppx':
|
||||
_fileType = 'Project';
|
||||
break;
|
||||
case 'mpp':
|
||||
_fileType = 'Project';
|
||||
break;
|
||||
case 'vsdx':
|
||||
_fileType = 'Visio';
|
||||
break;
|
||||
case 'vsd':
|
||||
_fileType = 'Visio';
|
||||
break;
|
||||
case 'vdx':
|
||||
_fileType = 'Visio';
|
||||
break;
|
||||
|
||||
default:
|
||||
_fileType = 'Other';
|
||||
break;
|
||||
}
|
||||
return Promise.resolve(_fileType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "d1bcb4f0-68d2-4442-843d-8e8e0e228171",
|
||||
"alias": "MyTasksWebPart",
|
||||
"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","TeamsTab","SharePointFullPage"],
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "SPFx WebParts" },
|
||||
"title": { "default": "My Tasks" },
|
||||
"description": { "default": "My Planner Tasks" },
|
||||
"officeFabricIconFontName": "TaskSolid",
|
||||
"properties": {
|
||||
"description": "My Tasks"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-property-pane';
|
||||
|
||||
import * as strings from 'MyTasksWebPartStrings';
|
||||
import MyTasks from './components/MyTasks/MyTasks';
|
||||
import { IMyTasksProps } from './components//MyTasks/IMyTasksProps';
|
||||
|
||||
export interface IMyTasksWebPartProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default class MyTasksWebPart extends BaseClientSideWebPart<IMyTasksWebPartProps> {
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IMyTasksProps > = React.createElement(
|
||||
MyTasks,
|
||||
{
|
||||
description: this.properties.description,
|
||||
context: this.context
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(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('description', {
|
||||
label: strings.DescriptionFieldLabel
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
import * as jsStyles from './AssignsStyles';
|
||||
import * as React from 'react';
|
||||
import InfiniteScroll from 'react-infinite-scroller';
|
||||
import spservices from '../../../../services/spservices';
|
||||
import styles from './Assigns.module.scss';
|
||||
import {
|
||||
Callout,
|
||||
DefaultPalette,
|
||||
FontWeights,
|
||||
Icon,
|
||||
IconButton,
|
||||
IFacepilePersona,
|
||||
IPersonaSharedProps,
|
||||
Label,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Persona,
|
||||
PersonaBase,
|
||||
PersonaSize,
|
||||
Stack,
|
||||
TextField,
|
||||
Spinner,
|
||||
SpinnerSize
|
||||
} from 'office-ui-fabric-react';
|
||||
import { FontSizes } from '@uifabric/fluent-theme/lib/fluent/FluentType';
|
||||
import { IAssignments } from '../../../../services/IAssignments';
|
||||
import { IAssignsProps } from './IAssignsProps';
|
||||
import { IAssignsState } from './IAssignsState';
|
||||
import { IGroupMember, IMember } from '../../../../services/IGroupMembers';
|
||||
|
||||
export class Assigns extends React.Component<IAssignsProps, IAssignsState> {
|
||||
private _spservices: spservices = this.props.spservice;
|
||||
private _membersSkipToken: string = undefined;
|
||||
private _unAssignsMembers: IMember[] = [];
|
||||
|
||||
constructor(props: IAssignsProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
unAssigns: [],
|
||||
assigns: [],
|
||||
hasError: false,
|
||||
hasMoreMembers: false,
|
||||
messageError: '',
|
||||
isloading: true
|
||||
};
|
||||
}
|
||||
|
||||
private _sortUnAssigns = (a: IGroupMember, b: IGroupMember) => {};
|
||||
|
||||
/**
|
||||
* Load more group members not assigned
|
||||
*/
|
||||
private _loadMoreMembers = async (ev: any) => {
|
||||
try {
|
||||
const unAssigns = await this._getunAssigns(this.props.plannerPlan.owner);
|
||||
this.setState({
|
||||
unAssigns: unAssigns,
|
||||
hasError: false,
|
||||
messageError: '',
|
||||
hasMoreMembers: this._membersSkipToken ? true : false
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, messageError: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether click member on
|
||||
*/
|
||||
private _onClickMember = (ev: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
alert(ev.currentTarget.getAttribute('data-memberId'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets assignments
|
||||
* @param assignments
|
||||
* @returns assignments
|
||||
*/
|
||||
private async _getAssignments(assignments: IAssignments): Promise<JSX.Element[]> {
|
||||
let persona: IPersonaSharedProps = undefined;
|
||||
let assignmentsKeys: string[] = [];
|
||||
let renderAssigns: JSX.Element[] = [];
|
||||
assignmentsKeys = Object.keys(assignments);
|
||||
for (const userId of assignmentsKeys) {
|
||||
try {
|
||||
const user = await this.props.spservice.getUser(userId);
|
||||
const userPhoto = await this.props.spservice.getUserPhoto(user.userPrincipalName);
|
||||
persona = {
|
||||
style: { paddingRight: 5, cursor: 'default' },
|
||||
text: user.displayName,
|
||||
imageUrl: userPhoto
|
||||
};
|
||||
|
||||
renderAssigns.push(
|
||||
<div className={styles.renderMemberItem} key={user.id} data-member-displayName={user.displayName}>
|
||||
<Persona
|
||||
{...persona}
|
||||
data-memberId={user.id}
|
||||
size={PersonaSize.size32}
|
||||
styles={jsStyles.memberPersonaStyle}
|
||||
onClick={this._onClickMember}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'ChromeClose', styles: { ...jsStyles.chromeCloseButtomStyle } }}
|
||||
styles={{ root: { paddingRight: 10 } }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
}
|
||||
return renderAssigns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets group members
|
||||
* @param groupId
|
||||
* @returns group members
|
||||
*/
|
||||
private async _getunAssigns(groupId: string): Promise<JSX.Element[]> {
|
||||
try {
|
||||
const assignmentsKeys = Object.keys(this.props.task.assignments);
|
||||
const unAssigns: IGroupMember = await this._spservices.getGroupMembers(
|
||||
this.props.plannerPlan.owner,
|
||||
this._membersSkipToken
|
||||
);
|
||||
|
||||
let persona: IPersonaSharedProps = undefined;
|
||||
let renderMembers: JSX.Element[] = [];
|
||||
|
||||
if (unAssigns && unAssigns['@odata.nextLink']) {
|
||||
const URLQueryString = new URLSearchParams(unAssigns['@odata.nextLink']);
|
||||
this._membersSkipToken = URLQueryString.get('$skiptoken');
|
||||
} else {
|
||||
this._membersSkipToken = undefined;
|
||||
}
|
||||
|
||||
const unAssignsMembers = unAssigns && unAssigns ? unAssigns.value : [];
|
||||
this._unAssignsMembers = this._unAssignsMembers.concat(unAssignsMembers);
|
||||
// Sort Members
|
||||
this._unAssignsMembers = this._unAssignsMembers.sort((a, b) => {
|
||||
if (a.displayName.toLocaleUpperCase() < b.displayName.toLocaleUpperCase()) return -1;
|
||||
if (a.displayName.toLocaleLowerCase() > b.displayName.toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
});
|
||||
// read group member and add to render array
|
||||
for (const member of this._unAssignsMembers) {
|
||||
// if (assignmentsKeys.indexOf(member.id) !== -1) continue;
|
||||
if (this._checkIfUserAssigned(member.id)) continue; // don't show members that are already assigned
|
||||
const userPhoto = await this.props.spservice.getUserPhoto(member.userPrincipalName);
|
||||
persona = {
|
||||
style: { paddingRight: 5 },
|
||||
text: member.displayName,
|
||||
imageUrl: userPhoto
|
||||
};
|
||||
|
||||
renderMembers.push(
|
||||
<div className={styles.renderMemberItem} key={member.id}>
|
||||
<Persona
|
||||
{...persona}
|
||||
data-memberId={member.id}
|
||||
size={PersonaSize.size32}
|
||||
styles={jsStyles.memberPersonaStyle}
|
||||
onClick={this._onClickMember}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// <Icon iconName={'ChromeClose'} styles={{ root: { paddingRight: 10, fontSize: FontSizes.size12 } }} />
|
||||
return renderMembers;
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether callout dismiss on
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* Determines whether callout dismiss on
|
||||
*/
|
||||
private _onCalloutDismiss = (ev: any) => {
|
||||
this.props.onDismiss();
|
||||
};
|
||||
|
||||
/**
|
||||
* Components did mount
|
||||
* @returns did mount
|
||||
*/
|
||||
public async componentDidMount(): Promise<void> {
|
||||
let unAssigns: JSX.Element[] = [];
|
||||
let assigns: JSX.Element[] = [];
|
||||
try {
|
||||
assigns = await this._getAssignments(this.props.task.assignments);
|
||||
unAssigns = await this._getunAssigns(this.props.plannerPlan.owner);
|
||||
this.setState({
|
||||
assigns: assigns,
|
||||
unAssigns: unAssigns,
|
||||
hasError: false,
|
||||
messageError: '',
|
||||
isloading: false,
|
||||
hasMoreMembers: this._membersSkipToken ? true : false
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({ unAssigns: unAssigns, assigns: assigns, hasError: true, messageError: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if user is member of unAssignsMembers
|
||||
*/
|
||||
private _checkIfUserIsMember = async (userId:string):Promise<boolean> => {
|
||||
|
||||
const foundUser = this._unAssignsMembers.filter( (user) => {
|
||||
return ( user.id === userId);
|
||||
}
|
||||
);
|
||||
console.log('found', foundUser);
|
||||
return foundUser.length > 0 ? true : false;
|
||||
}
|
||||
/**
|
||||
* Check if user assigned of assigns
|
||||
*/
|
||||
private _checkIfUserAssigned = (userId:string):boolean => {
|
||||
const assignmentsKeys = Object.keys(this.props.task.assignments);
|
||||
return (assignmentsKeys.indexOf(userId) !== -1) ? true: false;
|
||||
}
|
||||
/**
|
||||
* Determines whether search user on
|
||||
*/
|
||||
private _onSearchUser= async (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,newValue:string) => {
|
||||
try {
|
||||
if (newValue.length > 3){
|
||||
const users:IMember[] = await this._spservices.searchUsers(newValue);
|
||||
|
||||
console.log(users);
|
||||
for (const user of users){
|
||||
const found = await this._checkIfUserIsMember(user.id);
|
||||
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Components did update
|
||||
* @param prevProps
|
||||
* @param prevState
|
||||
*/
|
||||
public componentDidUpdate(prevProps: IAssignsProps, prevState: IAssignsState): void {}
|
||||
|
||||
public render(): React.ReactElement<IAssignsProps> {
|
||||
return (
|
||||
<div>
|
||||
<Callout
|
||||
target={this.props.target}
|
||||
isBeakVisible={false}
|
||||
role='dialog'
|
||||
onDismiss={this._onCalloutDismiss}
|
||||
calloutWidth={320}
|
||||
gapSpace={5}
|
||||
setInitialFocus={true}>
|
||||
<div className={styles.calloutHeader}>
|
||||
<TextField placeholder={'enter name or email'} borderless={true} styles={jsStyles.textFieldSearchStyles} onChange={this._onSearchUser}></TextField>
|
||||
</div>
|
||||
|
||||
<div className={styles.calloutContent}>
|
||||
{this.state.isloading ? (
|
||||
<Spinner size={SpinnerSize.small} label={'loading...'} />
|
||||
) : (
|
||||
<Stack styles={{ root: { height: '100%' } }}>
|
||||
{this.state.assigns.length > 0 && (
|
||||
<>
|
||||
<h4 style={{ margin: 10 }}>Assigned</h4>
|
||||
{this.state.assigns}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(this.state.unAssigns.length > 0 || this.state.hasMoreMembers) && ( // Has Member or has more pages to load, show unassigns
|
||||
<>
|
||||
<h4 style={{ margin: 10 }}>Unassigned</h4>
|
||||
|
||||
{this.state.hasError ? (
|
||||
<MessageBar messageBarType={MessageBarType.error}>{this.state.messageError}</MessageBar>
|
||||
) : (
|
||||
<InfiniteScroll
|
||||
pageStart={0}
|
||||
loadMore={this._loadMoreMembers}
|
||||
hasMore={this.state.hasMoreMembers}
|
||||
threshold={5}
|
||||
useWindow={false}>
|
||||
{this.state.unAssigns}
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.renderMemberItem {
|
||||
display: flex;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
width: '100%';
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.renderMemberItem:hover {
|
||||
background-color: $ms-color-neutralLight;
|
||||
|
||||
}
|
||||
|
||||
.calloutContent {
|
||||
height: 300px;
|
||||
padding: 0px 10px 10px 10px;
|
||||
margin-bottom: 10px;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
}
|
||||
.calloutHeader {
|
||||
padding: 10px 10px 15px 10px;
|
||||
}
|
||||
|
||||
.membersContainer {
|
||||
max-height: 250px;
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.assignsContainer {
|
||||
max-height: 250px;
|
||||
overflow: auto;
|
||||
margin: 5px;
|
||||
}
|
|
@ -0,0 +1,523 @@
|
|||
import * as jsStyles from './AssignsStyles';
|
||||
import * as React from 'react';
|
||||
import InfiniteScroll from 'react-infinite-scroller';
|
||||
import spservices from '../../../../services/spservices';
|
||||
import styles from './Assigns.module.scss';
|
||||
import * as strings from 'MyTasksWebPartStrings';
|
||||
import {
|
||||
Callout,
|
||||
DefaultPalette,
|
||||
FontWeights,
|
||||
Icon,
|
||||
IconButton,
|
||||
IFacepilePersona,
|
||||
IPersonaSharedProps,
|
||||
Label,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Persona,
|
||||
PersonaBase,
|
||||
PersonaSize,
|
||||
Stack,
|
||||
TextField,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Dialog,
|
||||
DialogType,
|
||||
DialogFooter
|
||||
} from 'office-ui-fabric-react';
|
||||
import { FontSizes } from '@uifabric/fluent-theme/lib/fluent/FluentType';
|
||||
import { IAssignments } from '../../../../services/IAssignments';
|
||||
import { IAssignsProps } from './IAssignsProps';
|
||||
import { IAssignsState } from './IAssignsState';
|
||||
import { IGroupMember, IMember } from '../../../../services/IGroupMembers';
|
||||
import { IAssign } from './IAssign';
|
||||
import { AssignMode } from './../Assigns/EAssignMode';
|
||||
import { stringIsNullOrEmpty } from '@pnp/pnpjs';
|
||||
|
||||
export class Assigns extends React.Component<IAssignsProps, IAssignsState> {
|
||||
private _spservices: spservices = this.props.spservice;
|
||||
private _membersSkipToken: string = undefined;
|
||||
private _unAssignsMembers: IMember[] = [];
|
||||
private _assigns: IMember[] = [];
|
||||
private _nonMembers: IMember[] = [];
|
||||
|
||||
constructor(props: IAssignsProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
unAssigns: [],
|
||||
assigns: [],
|
||||
nonMembers: [],
|
||||
hasError: false,
|
||||
hasMoreMembers: false,
|
||||
messageError: '',
|
||||
isloading: true,
|
||||
searchValue: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Load more group members not assigned
|
||||
*/
|
||||
private _loadMoreMembers = async (ev: any) => {
|
||||
try {
|
||||
await this._getunAssigns(this.props.plannerPlan.owner);
|
||||
const unAssigns = await this._renderUnAssigns(this._unAssignsMembers);
|
||||
this.setState({
|
||||
unAssigns: unAssigns,
|
||||
hasError: false,
|
||||
messageError: '',
|
||||
hasMoreMembers: this._membersSkipToken ? true : false
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, messageError: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether click member on
|
||||
*/
|
||||
private _onClickAddUnAssignMember = async (ev: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
ev.preventDefault();
|
||||
let renderAssigns: JSX.Element[] = [];
|
||||
let renderUnAssigns: JSX.Element[] = [];
|
||||
const memberId = ev.currentTarget.getAttribute('data-memberId');
|
||||
const user = this._unAssignsMembers.filter(unAssignMember => {
|
||||
return unAssignMember.id === memberId;
|
||||
});
|
||||
const idx = this._unAssignsMembers.indexOf(user[0]);
|
||||
const rtnMember = this._unAssignsMembers.splice(idx, 1);
|
||||
this._assigns.push(rtnMember[0]);
|
||||
renderAssigns = await this._renderAssigns(this._assigns);
|
||||
renderUnAssigns = await this._renderUnAssigns(this._unAssignsMembers);
|
||||
//alert(ev.currentTarget.getAttribute('data-memberId'));
|
||||
this.setState({ assigns: renderAssigns, unAssigns: renderUnAssigns, searchValue: '' });
|
||||
|
||||
//alert(ev.currentTarget.getAttribute('data-memberId'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether click assign non member on
|
||||
*/
|
||||
private _onClickAssignNonMember = async (ev: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
let renderAssigns: JSX.Element[] = [];
|
||||
let renderNonMembers: JSX.Element[] = [];
|
||||
let renderUnAssigns: JSX.Element[] = [];
|
||||
const memberId = ev.currentTarget.getAttribute('data-memberId');
|
||||
const user = this._nonMembers.filter(nonMember => {
|
||||
return nonMember.id === memberId;
|
||||
});
|
||||
const idx = this._nonMembers.indexOf(user[0]);
|
||||
const rtnMember = this._nonMembers.splice(idx, 1);
|
||||
this._assigns.push(rtnMember[0]);
|
||||
renderAssigns = await this._renderAssigns(this._assigns) ;
|
||||
renderUnAssigns = await this._renderUnAssigns(this._unAssignsMembers);
|
||||
renderNonMembers = await this._renderNonMembers(this._nonMembers);
|
||||
//alert(ev.currentTarget.getAttribute('data-memberId'));
|
||||
this.setState({ assigns: renderAssigns, unAssigns: renderUnAssigns, nonMembers: renderNonMembers, searchValue: '' });
|
||||
}
|
||||
|
||||
private _onClickRemoveAssign = async (ev: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
let renderAssigns: JSX.Element[] = [];
|
||||
let renderUnAssigns: JSX.Element[] = [];
|
||||
const memberId = ev.currentTarget.getAttribute('data-memberId');
|
||||
const user = this._assigns.filter(assignMember => {
|
||||
return assignMember.id === memberId;
|
||||
});
|
||||
const idx = this._assigns.indexOf(user[0]);
|
||||
const rtnMember = this._assigns.splice(idx, 1);
|
||||
this._unAssignsMembers.push(rtnMember[0]);
|
||||
renderAssigns = await this._renderAssigns(this._assigns);
|
||||
renderUnAssigns = await this._renderUnAssigns(this._unAssignsMembers);
|
||||
//alert(ev.currentTarget.getAttribute('data-memberId'));
|
||||
this.setState({ assigns: renderAssigns, unAssigns: renderUnAssigns, searchValue: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets assignments
|
||||
* @param assignments
|
||||
* @returns assignments
|
||||
*/
|
||||
private async _getAssignments(assignments: IAssignments): Promise<void> {
|
||||
let assignmentsKeys: string[] = [];
|
||||
|
||||
assignmentsKeys = Object.keys(assignments);
|
||||
for (const userId of assignmentsKeys) {
|
||||
try {
|
||||
const user = await this.props.spservice.getUser(userId);
|
||||
|
||||
this._assigns.push(user);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render non members of assigns
|
||||
*/
|
||||
private _renderNonMembers = async (nonMembers: IMember[]): Promise<JSX.Element[]> => {
|
||||
let persona: IPersonaSharedProps = undefined;
|
||||
let renderNonMembers: JSX.Element[] = [];
|
||||
try {
|
||||
for (const user of nonMembers) {
|
||||
const userPhoto = await this.props.spservice.getUserPhoto(user.userPrincipalName);
|
||||
persona = {
|
||||
style: { paddingRight: 5, cursor: 'default' },
|
||||
text: user.displayName,
|
||||
imageUrl: userPhoto
|
||||
};
|
||||
|
||||
renderNonMembers.push(
|
||||
<div className={styles.renderMemberItem} key={user.id}>
|
||||
<Persona
|
||||
{...persona}
|
||||
data-memberId={user.id}
|
||||
size={PersonaSize.size32}
|
||||
styles={jsStyles.memberPersonaStyle}
|
||||
onClick={this._onClickAssignNonMember}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return renderNonMembers;
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render assigns of assigns
|
||||
*/
|
||||
private _renderAssigns = async (assigns: IMember[]): Promise<JSX.Element[]> => {
|
||||
let persona: IPersonaSharedProps = undefined;
|
||||
let renderAssigns: JSX.Element[] = [];
|
||||
try {
|
||||
for (const user of assigns) {
|
||||
const userPhoto = await this.props.spservice.getUserPhoto(user.userPrincipalName);
|
||||
persona = {
|
||||
style: { paddingRight: 5, cursor: 'default' },
|
||||
text: user.displayName,
|
||||
imageUrl: userPhoto
|
||||
};
|
||||
|
||||
renderAssigns.push(
|
||||
<div className={styles.renderMemberItem} key={user.id}>
|
||||
<Persona
|
||||
{...persona}
|
||||
data-memberId={user.id}
|
||||
size={PersonaSize.size32}
|
||||
styles={jsStyles.memberPersonaStyle}
|
||||
// onClick={this._onClickMember}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'ChromeClose', styles: { ...jsStyles.chromeCloseButtomStyle } }}
|
||||
styles={{ root: { paddingRight: 10 } }}
|
||||
data-memberId={user.id}
|
||||
onClick={this._onClickRemoveAssign}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return renderAssigns;
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render unassigns
|
||||
*/
|
||||
private _renderUnAssigns = async (unAssignsMembers: IMember[]): Promise<JSX.Element[]> => {
|
||||
let persona: IPersonaSharedProps = undefined;
|
||||
let renderMembers: JSX.Element[] = [];
|
||||
try {
|
||||
// read group member and add to render array
|
||||
for (const member of unAssignsMembers) {
|
||||
// If in Edit Task Mode check Assigns
|
||||
if (this.props.AssignMode === AssignMode.Edit) {
|
||||
// if (await this._checkIfUserAssigned(member.id)) continue;
|
||||
} // don't show members that are already assigned
|
||||
const userPhoto = await this.props.spservice.getUserPhoto(member.userPrincipalName);
|
||||
persona = {
|
||||
style: { paddingRight: 5 },
|
||||
text: member.displayName,
|
||||
imageUrl: userPhoto
|
||||
};
|
||||
|
||||
renderMembers.push(
|
||||
<div className={styles.renderMemberItem} key={member.id}>
|
||||
<Persona
|
||||
{...persona}
|
||||
data-memberId={member.id}
|
||||
size={PersonaSize.size32}
|
||||
styles={jsStyles.memberPersonaStyle}
|
||||
onClick={this._onClickAddUnAssignMember}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// <Icon iconName={'ChromeClose'} styles={{ root: { paddingRight: 10, fontSize: FontSizes.size12 } }} />
|
||||
return renderMembers;
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets group members
|
||||
* @param groupId
|
||||
* @returns group members
|
||||
*/
|
||||
private async _getunAssigns(groupId: string): Promise<void> {
|
||||
try {
|
||||
const groupMembers: IGroupMember = await this._spservices.getGroupMembers(groupId, this._membersSkipToken);
|
||||
|
||||
if (groupMembers && groupMembers['@odata.nextLink']) {
|
||||
const URLQueryString = new URLSearchParams(groupMembers['@odata.nextLink']);
|
||||
this._membersSkipToken = URLQueryString.get('$skiptoken');
|
||||
} else {
|
||||
this._membersSkipToken = undefined;
|
||||
}
|
||||
// skip users already assigned
|
||||
if (groupMembers && groupMembers.value ){
|
||||
for ( const groupMember of groupMembers.value){
|
||||
const isAssigned = await this._checkIfUserAssigned(groupMember.id);
|
||||
if (isAssigned){
|
||||
continue;
|
||||
}else{
|
||||
this._unAssignsMembers.push(groupMember);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// const unAssignsMembers = unAssigns && unAssigns.value ? unAssigns.value : [];
|
||||
// this._unAssignsMembers = this._unAssignsMembers.concat(unAssignsMembers);
|
||||
// Sort Members
|
||||
this._unAssignsMembers = this._unAssignsMembers.sort((a, b) => {
|
||||
if (a.displayName.toLocaleUpperCase() < b.displayName.toLocaleUpperCase()) return -1;
|
||||
if (a.displayName.toLocaleLowerCase() > b.displayName.toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether callout dismiss on
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* Determines whether callout dismiss on
|
||||
*/
|
||||
private _onCalloutDismiss = (ev: any) => {
|
||||
this.props.onDismiss(this._assigns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Components did mount
|
||||
* @returns did mount
|
||||
*/
|
||||
public async componentDidMount(): Promise<void> {
|
||||
let unAssigns: JSX.Element[] = [];
|
||||
let assigns: JSX.Element[] = [];
|
||||
try {
|
||||
/*
|
||||
if (this.props.AssignMode === AssignMode.Edit) {
|
||||
await this._getAssignments(this.props.task.assignments);
|
||||
}else{
|
||||
this._assigns = this.props.assigns;
|
||||
}*/
|
||||
this._assigns = this.props.assigns;
|
||||
assigns = await this._renderAssigns(this._assigns);
|
||||
|
||||
await this._getunAssigns(this.props.plannerPlan.owner);
|
||||
unAssigns = await this._renderUnAssigns(this._unAssignsMembers);
|
||||
this.setState({
|
||||
assigns: assigns,
|
||||
unAssigns: unAssigns,
|
||||
hasError: false,
|
||||
messageError: '',
|
||||
isloading: false,
|
||||
hasMoreMembers: this._membersSkipToken ? true : false
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({ unAssigns: unAssigns, assigns: assigns, hasError: true, messageError: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is member of unAssignsMembers
|
||||
*/
|
||||
private _checkIfUserIsMember = async (userId: string): Promise<boolean> => {
|
||||
const foundUser = this._unAssignsMembers.filter(user => {
|
||||
return user.id === userId;
|
||||
});
|
||||
return foundUser.length > 0 ? true : false;
|
||||
}
|
||||
/**
|
||||
* Check if user assigned of assigns
|
||||
*/
|
||||
private _checkIfUserAssigned = async (userId: string): Promise<boolean> => {
|
||||
const user = this._assigns.filter(assignMember => {
|
||||
return assignMember.id === userId;
|
||||
});
|
||||
/*
|
||||
if (user.length > 0) {
|
||||
const idx = this._unAssignsMembers.indexOf(user[0]);
|
||||
this._unAssignsMembers.splice(idx, 1);
|
||||
}*/
|
||||
return user.length > 0 ? true : false;
|
||||
}
|
||||
/**
|
||||
* Determines whether search user on
|
||||
*/
|
||||
private _onSearchUser = async (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue: string) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.setState({searchValue: newValue,});
|
||||
try {
|
||||
let renderAssigns: JSX.Element[] = [];
|
||||
let renderUnAssigns: JSX.Element[] = [];
|
||||
let renderNonMembers: JSX.Element[] = [];
|
||||
|
||||
|
||||
if (newValue.length > 2) {
|
||||
const users: IMember[] = await this._spservices.searchUsers(newValue);
|
||||
const unAssignMembers: IMember[] = [];
|
||||
const assigns: IMember[] = [];
|
||||
this._nonMembers = [];
|
||||
|
||||
for (const user of users) {
|
||||
// const userAssigned = this.props.AssignMode === AssignMode.Edit ? await this._checkIfUserAssigned(user.id) : false;
|
||||
const userAssigned = await this._checkIfUserAssigned(user.id) ;
|
||||
if (userAssigned) {
|
||||
assigns.push(user);
|
||||
} else {
|
||||
const found = await this._checkIfUserIsMember(user.id);
|
||||
if (found) {
|
||||
unAssignMembers.push(user);
|
||||
} else {
|
||||
this._nonMembers.push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderAssigns = this.props.AssignMode === AssignMode.Edit ? await this._renderAssigns(assigns) : [];
|
||||
renderAssigns = await this._renderAssigns(assigns);
|
||||
renderUnAssigns = await this._renderUnAssigns(unAssignMembers);
|
||||
renderNonMembers = await this._renderNonMembers(this._nonMembers);
|
||||
|
||||
this.setState({
|
||||
assigns: renderAssigns,
|
||||
unAssigns: renderUnAssigns,
|
||||
searchValue: newValue,
|
||||
nonMembers: renderNonMembers,
|
||||
hasError: false,
|
||||
messageError: ''
|
||||
});
|
||||
}
|
||||
|
||||
if (newValue.length <=2) {
|
||||
renderAssigns = await this._renderAssigns(this._assigns);
|
||||
renderUnAssigns = await this._renderUnAssigns(this._unAssignsMembers);
|
||||
this.setState({
|
||||
assigns: renderAssigns,
|
||||
unAssigns: renderUnAssigns,
|
||||
searchValue: newValue,
|
||||
nonMembers: renderNonMembers,
|
||||
hasError: false,
|
||||
messageError: ''
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
this.setState({ hasError: true, messageError: error.message });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Components did update
|
||||
* @param prevProps
|
||||
* @param prevState
|
||||
*/
|
||||
public componentDidUpdate(prevProps: IAssignsProps, prevState: IAssignsState): void {}
|
||||
|
||||
public render(): React.ReactElement<IAssignsProps> {
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
hidden={false}
|
||||
onDismiss={this._onCalloutDismiss}
|
||||
minWidth={350}
|
||||
title={strings.AssignsLabel}
|
||||
dialogContentProps={{
|
||||
type: DialogType.normal
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: false,
|
||||
styles: { main: { maxWidth: 350 } }
|
||||
}}>
|
||||
<div className={styles.calloutHeader}>
|
||||
<TextField
|
||||
value={this.state.searchValue}
|
||||
placeholder={strings.TypeUserOrEmailLabel}
|
||||
borderless={true}
|
||||
styles={jsStyles.textFieldSearchStyles}
|
||||
onChange={this._onSearchUser}></TextField>
|
||||
</div>
|
||||
|
||||
<div className={styles.calloutContent}>
|
||||
{this.state.isloading ? (
|
||||
<Spinner size={SpinnerSize.small} label={strings.LoadingAssignLabel} />
|
||||
) : (
|
||||
<Stack styles={{ root: { height: '100%' } }}>
|
||||
{this.state.assigns.length > 0 && (
|
||||
<>
|
||||
<h4 style={{ margin: 10 }}>{strings.AssignedLabel}</h4>
|
||||
{this.state.assigns}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(this.state.unAssigns.length > 0 || this.state.hasMoreMembers) && ( // Has Member or has more pages to load, show unassigns
|
||||
<>
|
||||
<h4 style={{ margin: 10 }}>{strings.UnassignedLabel}</h4>
|
||||
|
||||
{this.state.hasError ? (
|
||||
<MessageBar messageBarType={MessageBarType.error}>{this.state.messageError}</MessageBar>
|
||||
) : (
|
||||
<InfiniteScroll
|
||||
pageStart={0}
|
||||
loadMore={this._loadMoreMembers}
|
||||
hasMore={this.state.hasMoreMembers}
|
||||
threshold={15}
|
||||
useWindow={false}>
|
||||
{this.state.unAssigns}
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.state.nonMembers.length > 0 && (
|
||||
<>
|
||||
<h4 style={{ margin: 10 }}>{strings.NonMembersLabel}</h4>
|
||||
{this.state.nonMembers}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
import { FontSizes, FontWeights, DefaultPalette, getTheme } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { CommunicationColors, } from '@uifabric/fluent-theme/lib/fluent/FluentColors';
|
||||
import {
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
IStackItemStyles,
|
||||
ITextFieldStyles,
|
||||
ITextFieldSubComponentStyles,
|
||||
IModalStyles,
|
||||
ImageLoadState,
|
||||
IDatePickerStyles,
|
||||
ITextFieldProps,
|
||||
IStyle,
|
||||
IButtonStyles,
|
||||
calculatePrecision,
|
||||
IDropdownStyles,
|
||||
IPersonaStyles,
|
||||
IIconStyles,
|
||||
} from 'office-ui-fabric-react';
|
||||
|
||||
// Styles definition
|
||||
|
||||
export const memberPersonaStyle: IPersonaStyles = {
|
||||
root: {flexGrow: 7, padding: 5 , cursor: 'pointer'},
|
||||
details: {},
|
||||
primaryText:{},
|
||||
secondaryText:{},
|
||||
tertiaryText:{},
|
||||
optionalText:{},
|
||||
textContent:{},
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const stackStyles: IStackStyles = {
|
||||
root: {
|
||||
background: DefaultPalette.themeTertiary
|
||||
}
|
||||
};
|
||||
export const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
padding: 5,
|
||||
display: 'flex',
|
||||
width: 172,
|
||||
height: 32,
|
||||
fontWeight: FontWeights.regular,
|
||||
}
|
||||
};
|
||||
|
||||
export const stackTokens: IStackTokens = {
|
||||
childrenGap: 10,
|
||||
|
||||
};
|
||||
|
||||
export const textFielStartDateDatePickerStyles: ITextFieldProps = {
|
||||
styles: {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const textFielDueDateDatePickerStyles: ITextFieldProps = {
|
||||
styles: {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export const textFieldSearchStyles: ITextFieldStyles = {
|
||||
field: { backgroundColor: '#f3f2f1', borderBottomStyle: "solid", borderBottomWidth: 2, borderBottomColor: getTheme().palette.themePrimary},
|
||||
root: { backgroundColor: '#f3f2f1'},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const textFieldCheckListItem: ITextFieldStyles = {
|
||||
field: { },
|
||||
root: {width:'100%', height:32, marginRight:15, },
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: { backgroundColor: 'white'},
|
||||
suffix: { backgroundColor: 'white'},
|
||||
wrapper: {selectors:{ [':hover']: { borderWidth: 1,borderStyle:'solid', borderColor: DefaultPalette.themePrimary}}},
|
||||
subComponentStyles: undefined,
|
||||
|
||||
};
|
||||
|
||||
export const textFieldStylesTaskName: ITextFieldStyles = {
|
||||
field: { backgroundColor: DefaultPalette.neutralLighter },
|
||||
root: {},
|
||||
description: {},
|
||||
errorMessage: {},
|
||||
fieldGroup: {},
|
||||
icon: {},
|
||||
prefix: {},
|
||||
suffix: {},
|
||||
wrapper: {},
|
||||
subComponentStyles: undefined
|
||||
};
|
||||
|
||||
export const modalStyles: IModalStyles = {
|
||||
main: { minWidth: 400 ,maxWidth: 450, },
|
||||
root: {},
|
||||
keyboardMoveIcon: {},
|
||||
keyboardMoveIconContainer: {},
|
||||
layer: {},
|
||||
scrollableContent: {}
|
||||
};
|
||||
|
||||
export const datePickerStartDateStyles: IDatePickerStyles = {
|
||||
callout: {},
|
||||
icon: {},
|
||||
root: { marginTop:0},
|
||||
textField: { backgroundColor: '#f4f4f4', borderWidth:0}
|
||||
};
|
||||
|
||||
export const textFieldStylesdatePicker: ITextFieldProps = {
|
||||
style: { display: 'flex', justifyContent: 'flex-start', marginLeft: 15 },
|
||||
iconProps: { style: { left: 0 } }
|
||||
};
|
||||
|
||||
export const peoplePicker: IStyle = {
|
||||
backgroundColor: DefaultPalette.neutralLighter
|
||||
};
|
||||
|
||||
export const addMemberButton: IButtonStyles = {
|
||||
root: { marginLeft: 0, paddingLeft: 0, marginTop: 0, fontSize: FontSizes.medium },
|
||||
textContainer: {
|
||||
fontSize: FontSizes.medium,
|
||||
fontWeight: 'normal',
|
||||
color: '#666666',
|
||||
marginLeft: 5
|
||||
}
|
||||
};
|
||||
|
||||
export const dropDownBucketStyles: IDropdownStyles = {
|
||||
|
||||
root: { margin: 0 } ,
|
||||
title: {backgroundColor: '#f4f4f4', borderWidth:0},
|
||||
callout: {},
|
||||
caretDown: {},
|
||||
caretDownWrapper: {},
|
||||
dropdown:{},
|
||||
dropdownDivider: {},
|
||||
dropdownItem: {},
|
||||
dropdownItemDisabled: {},
|
||||
dropdownItemHeader:{},
|
||||
dropdownItemHidden: {},
|
||||
dropdownItemSelected:{},
|
||||
dropdownItemSelectedAndDisabled:{},
|
||||
dropdownItems:{},
|
||||
dropdownItemsWrapper:{},
|
||||
dropdownOptionText:{},
|
||||
errorMessage:{},
|
||||
label:{},
|
||||
panel:{},
|
||||
subComponentStyles: undefined,
|
||||
};
|
||||
|
||||
export const dropDownProgressStyles: IDropdownStyles = {
|
||||
|
||||
root: { margin: 0 } ,
|
||||
title: {backgroundColor: '#f4f4f4', borderWidth:0},
|
||||
callout: {},
|
||||
caretDown: {},
|
||||
caretDownWrapper: {},
|
||||
dropdown:{},
|
||||
dropdownDivider: {},
|
||||
dropdownItem: {},
|
||||
dropdownItemDisabled: {},
|
||||
dropdownItemHeader:{},
|
||||
dropdownItemHidden: {},
|
||||
dropdownItemSelected:{},
|
||||
dropdownItemSelectedAndDisabled:{},
|
||||
dropdownItems:{},
|
||||
dropdownItemsWrapper:{},
|
||||
dropdownOptionText:{},
|
||||
errorMessage:{},
|
||||
label:{},
|
||||
panel:{},
|
||||
subComponentStyles: undefined,
|
||||
};
|
||||
|
||||
export const chromeCloseButtomStyle: IIconStyles = {
|
||||
root: {
|
||||
fontSize: FontSizes.smallPlus,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export enum AssignMode {
|
||||
Add,
|
||||
Edit
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface IAssign {
|
||||
id:string;
|
||||
displayName: string;
|
||||
photo:string;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { IPlannerPlan } from '../../../../services/IPlannerPlan';
|
||||
import { ITask } from '../../../../services/ITask';
|
||||
import spservices from '../../../../services/spservices';
|
||||
import { IPlannerPlanExtended } from '../../../../services/IPlannerPlanExtended';
|
||||
import { AssignMode} from './EAssignMode';
|
||||
import { IMember } from '../../../../services/IGroupMembers';
|
||||
|
||||
export interface IAssignsProps {
|
||||
|
||||
onDismiss: (assigns?:IMember[]) => void;
|
||||
target?: HTMLElement;
|
||||
task?: ITask;
|
||||
plannerPlan:IPlannerPlanExtended;
|
||||
spservice: spservices;
|
||||
AssignMode?: AssignMode;
|
||||
assigns?: IMember[];
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { IAssign } from "./IAssign";
|
||||
|
||||
export interface IAssignsState{
|
||||
unAssigns: JSX.Element[];
|
||||
assigns:JSX.Element[];
|
||||
nonMembers:JSX.Element[];
|
||||
hasMoreMembers:boolean;
|
||||
hasError:boolean;
|
||||
messageError: string;
|
||||
isloading: boolean;
|
||||
searchValue:string;
|
||||
}
|