Merge pull request #1067 from joaojmendes/My-Tasks

React - My Tasks web part
This commit is contained in:
Laura Kokkarinen 2019-11-19 08:48:12 -05:00 committed by GitHub
commit 225ecb3a16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 58703 additions and 0 deletions

View File

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

32
samples/react-mytasks/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,10 @@
{
"extends": "stylelint-config-standard",
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true
}
}

View File

@ -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"
}
}

View File

@ -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" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

View File

@ -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'

View File

@ -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"
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"includeExtensions": [
"png",
"jpg",
"svg"
]
}

View File

@ -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 -->"
}

View File

@ -0,0 +1 @@
{"preset":"@voitanos/jest-preset-spfx-react16","rootDir":"../src","coverageReporters":["text","json","lcov","text-summary","cobertura"],"reporters":["default","jest-junit"]}

View File

@ -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"
}
}

View File

@ -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/"
}
}

View File

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

79
samples/react-mytasks/gulpfile.js vendored Normal file
View File

@ -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);

21802
samples/react-mytasks/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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>
);
}
}

View File

@ -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: {}
};

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
import { ITaskDetails } from "../../services/ITaskDetails";
export interface IAddLinkState {
hideDialog:boolean;
disableSaveButton:boolean;
link:string;
linkLabel:string;
}

View File

@ -0,0 +1 @@
export * from './AddLink';

View File

@ -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>
);
}
}

View File

@ -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
}
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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: {}
};

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
import { ITaskDetails } from "../../services/ITaskDetails";
export interface IEditLinkState {
hideDialog:boolean;
disableSaveButton:boolean;
link:string;
linkLabel:string;
}

View File

@ -0,0 +1 @@
export * from './EditLink';

View File

@ -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>
);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
export interface IUploadFileState {
isUploading:boolean;
percent:number;
}

View File

@ -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>
);
}
}

View File

@ -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
}
};

View File

@ -0,0 +1 @@
export * from './UploadFile';

View File

@ -0,0 +1,6 @@
export interface IListViewItems {
FileLeafRef: string;
Modified: string;
fileTypeImageUrl?: string;
fileUrl:string;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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'
}
}
}
});

View File

@ -0,0 +1,3 @@
export * from './IUploadFromSharePointProps';
export * from './IUploadFromSharePointState';
export * from './UploadFromSharePoint';

View File

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

View File

@ -0,0 +1,3 @@
export interface IAppliedCategories {
[key:string]: boolean;
}

View File

@ -0,0 +1,8 @@
import {IPlannerAssignment} from './IPlannerAssignment';
/**
* Assignments
*/
export interface IAssignments {
[key:string] : IPlannerAssignment;
}

View File

@ -0,0 +1,9 @@
export interface IBucket {
'@odata.etag': string;
name: string;
planId: string;
orderHint: string;
id: string;
}

View File

@ -0,0 +1,8 @@
export interface ICheckListItem {
key: string;
isChecked: boolean;
lastModifiedBy?: string;
lastModifiedByDateTime?: string;
orderHint?: string;
title: string;
}

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -0,0 +1,10 @@
import {IUser} from './IUser';
export interface IIdentitySet {
application: Application;
device: Application;
user: IUser;
}
interface Application {
'@odata.type': string;
}

View File

@ -0,0 +1,7 @@
import {IUser} from './IUser';
export interface IPlannerAssignment {
"@odata.type"?:string;
assignedDateTime?: string;
orderHint: string;
assignedBy?: IUser;
}

View File

@ -0,0 +1,7 @@
export interface IPlannerBucket {
'@odata.etag': string;
name: string;
planId: string;
orderHint: string;
id: string;
}

View File

@ -0,0 +1,13 @@
import { IUser } from "./IUser";
export interface IPlannerPlan {
createdBy?: IUser;
createdDateTime: string;
id: string;
owner: string;
title: string;
}

View File

@ -0,0 +1,8 @@
export interface IPlannerPlanDetails{
categoryDescriptions: {[key:string]: string};
id: string;
sharedWith: {[key:string]: boolean};
}

View File

@ -0,0 +1,4 @@
import { IPlannerPlan} from './IPlannerPlan';
export interface IPlannerPlanExtended extends IPlannerPlan {
planPhoto?: string;
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
export interface ITaskCheckListItem {
[key:string]: {
"@odata.type":string;
isChecked: boolean;
lastModifiedBy?: string;
lastModifiedByDateTime?: string;
orderHint: string;
title: string ; };
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export interface ITaskProperty {
[property:string]: string;
}

View File

@ -0,0 +1,4 @@
export interface IUser {
displayName?: any;
id: string;
}

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}
}

View File

@ -0,0 +1 @@
export * from './utilities';

View File

@ -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);
}
}

View File

@ -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"
}
}]
}

View File

@ -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
})
]
}
]
}
]
};
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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,
}
};

View File

@ -0,0 +1,4 @@
export enum AssignMode {
Add,
Edit
}

View File

@ -0,0 +1,5 @@
export interface IAssign {
id:string;
displayName: string;
photo:string;
}

View File

@ -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[];
}

View File

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More