New -React-Calendar Web part (#859)

* commit first

* Update README.md

* Update README.md

* first commit

* Change Docs

* Update README.md

* Update README.md

* Update Calendar Web Part

* Update README.md

* Update README.md

* Update Docs

* Update - Suport Create Event in SiteTimeZone

* update Docs

* update docs

* Update comments

* upd default start/end dates on prop panel

* update Prop

* upd default props panel

* upd comments
This commit is contained in:
joaojmendes 2019-05-04 15:58:48 +01:00 committed by Vesa Juvonen
parent ca0450c9c0
commit b0204bd14b
60 changed files with 24069 additions and 4 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-calendar/.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,32 @@
{
"@pnp/generator-spfx": {
"framework": "react",
"pnpFramework": "reactjs.plus",
"pnp-libraries": [
"jquery@3",
"@pnp/pnpjs",
"@pnp/spfx-property-controls",
"@pnp/spfx-controls-react"
],
"pnp-ci": [
"azure"
],
"pnp-vetting": [
"webpack-analyzer",
"stylelint"
],
"spfxenv": "spo"
},
"@microsoft/generator-sharepoint": {
"environment": "spo",
"framework": "react",
"plusBeta": true,
"isCreatingSolution": true,
"version": "1.8.0",
"libraryName": "react-calendar",
"libraryId": "3a13208b-3874-4036-9262-4edd22e88187",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,125 @@
# React Calendar
## Summary
This Web Part allows you to manage events in a calendar.
Uses a list of existing calendars on any website.
The location and name of the list and the dates of the events to be displayed are defined in the properties of the web part.
The Events are created in Site TimeZone, defined in site Regional Settings.
Each category has its own color that is generated in the load.
The Web Part checks the user's permissions for the View, Add, Edit, and Delete events.
The Web Part does not show recurring events, I will work on it soon.
##
![callendar](/samples/react-calendar/assets/animatevideo.gif)
## Web Part - Screenshots
![callendar](/samples/react-calendar//assets/screen1.png)
![callendar](/samples/react-calendar/assets/screen1.0.jpg)
![callendar](/samples/react-calendar/assets/screen1.1.png)
![callendar](/samples/react-calendar/assets/screen1.2.png)
![callendar](/samples/react-calendar/assets/screen1.3.png)
![callendar](/samples/react-calendar//assets/screen1.4.png)
![callendar](/samples/react-calendar//assets/screen2.png)
![callendar](/samples/react-calendar/assets/screen3.png)
![callendar](/samples/react-calendar//assets/screen4.png)
![callendar](/samples/react-calendar/assets/screen5.png)
![callendar](/samples/react-calendar//assets/screen6.png)
![callendar](/samples/react-calendar//assets/screen7.png)
![callendar](/samples/react-calendar/assets/screen8.png)
![callendar](/samples/react-calendar//assets/screen9.png)
##
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
> Update accordingly as needed.
## WebPart Properties
Property |Type|Required| comments
--------------------|----|--------|----------
Site Url of Calendar List | Text| yes|
Calendar list| Text| yes| this is filled with all list of type "event list" created
Start Date | Date | yes | Event Date
End Date| Date| yes | Event Date
## Solution
The Web Part Use PnPjs library, Office-ui-fabric-react components. react Big-Calendar Compoment
Solution|Author(s)
--------|---------
Calendar Web Part|João Mendes
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|April 25, 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
- in the command line run:
- `npm install`
- `gulp build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- `Add to AppCatalog and deploy`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

View File

@ -0,0 +1,71 @@
resources:
- repo: self
trigger:
- master
- develop
queue:
name: Hosted VS2017
demands:
- npm
- node.js
steps:
#install node 8.x
- task: NodeTool@0
displayName: 'Use Node 8.x'
inputs:
versionSpec: 8.x
checkLatest: true
#install nodejs modules with npm
- task: Npm@1
displayName: 'npm install'
inputs:
workingDir: '$(Build.SourcesDirectory)'
verbose: false
#start unit tests
- task: Gulp@0
displayName: 'gulp test'
inputs:
gulpFile: '$(Build.SourcesDirectory)/gulpfile.js'
targets: test
publishJUnitResults: true
testResultsFiles: '**/test-*.xml'
#publish test results
- task: PublishCodeCoverageResults@1
displayName: 'Publish Code Coverage Results $(Build.SourcesDirectory)/temp/coverage/cobertura/cobertura.xml'
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(Build.SourcesDirectory)/temp/coverage/cobertura/cobertura.xml'
reportDirectory: '$(Build.SourcesDirectory)/temp/coverage/cobertura'
#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": {
"calendar-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/calendar/CalendarWebPart.js",
"manifest": "./src/webparts/calendar/CalendarWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"CalendarWebPartStrings": "lib/webparts/calendar/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/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 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-calendar",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,4 @@
{
"preset": "@voitanos/jest-preset-spfx-react16",
"rootDir": "../src"
}

View File

@ -0,0 +1,30 @@
"use strict";
var existingKarmaConfig = require('@microsoft/sp-build-web/lib/karma/karma.config');
var junitReporter = require('karma-junit-reporter');
module.exports = function (config) {
existingKarmaConfig(config);
config.reporters.push('junit');
config.set({
basePath: './..',
});
config.junitReporter = {
outputDir: 'temp/', // results will be saved as $outputDir/$browserName.xml
outputFile: 'test-results.xml', // if included, results will be saved as $outputDir/$browserName/$outputFile
suite: 'karma', // suite will become the package name attribute in xml testsuite element
useBrowserName: true, // add browser name to report and classes names
};
var coberturaSubDir = 'cobertura';
var coverageSubDir = 'lcov';
var coberturaFileName = 'cobertura.xml';
config.coverageReporter.reporters.push({type: 'cobertura', subdir: './' + coberturaSubDir, file: coberturaFileName});
config.coverageReporter.reporters.push({
type: 'lcov',
subdir: './' + coverageSubDir + '/',
file: 'lcov.info'
});
config.browserNoActivityTimeout = 60000;
config.plugins.push(junitReporter);
};

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-calendar-client-side-solution",
"id": "3a13208b-3874-4036-9262-4edd22e88187",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-calendar.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,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

93
samples/react-calendar/gulpfile.js vendored Normal file
View File

@ -0,0 +1,93 @@
'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);
/**
* Continuous Integration
*/
const buildConfig = build.getConfig();
const karmaTaskCandidates = buildConfig.uniqueTasks.filter(task => task.name === 'karma');
if (karmaTaskCandidates && karmaTaskCandidates.length > 0) {
const karmaTask = karmaTaskCandidates[0];
karmaTask.taskConfig.configPath = './config/karma.config.js';
}

20664
samples/react-calendar/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
{
"name": "react-calendar",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"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/rush-stack-compiler-3.2": "^0.3.6",
"@microsoft/sp-core-library": "1.8.0-plusbeta",
"@microsoft/sp-lodash-subset": "1.8.0-plusbeta",
"@microsoft/sp-office-ui-fabric-core": "1.8.0-plusbeta",
"@microsoft/sp-property-pane": "1.8.0-plusbeta",
"@microsoft/sp-webpart-base": "1.8.0-plusbeta",
"@pnp/pnpjs": "^1.3.0",
"@pnp/spfx-controls-react": "1.12.0",
"@pnp/spfx-property-controls": "1.14.1",
"@types/draft-js": "^0.10.30",
"@types/es6-promise": "0.0.33",
"@types/globalize": "0.0.34",
"@types/jquery": "^3.3.29",
"@types/react": "16.7.22",
"@types/react-big-calendar": "^0.20.13",
"@types/react-dom": "16.0.5",
"@types/webpack-env": "1.13.1",
"draft-js": "^0.10.5",
"draftjs-to-html": "^0.8.4",
"globalize": "^1.4.2",
"immutable": "^4.0.0-rc.12",
"jquery": "^3.3.1",
"moment": "^2.24.0",
"moment-timezone": "^0.5.25",
"react": "16.7.0",
"react-big-calendar": "^0.20.4",
"react-dom": "16.7.0",
"react-draft-wysiwyg": "^1.13.2",
"typescript": "^3.2.4"
},
"resolutions": {
"@types/react": "16.4.2"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-2.7": "0.4.0",
"@microsoft/sp-build-web": "1.8.0-plusbeta",
"@microsoft/sp-module-interfaces": "1.8.0-plusbeta",
"@microsoft/sp-tslint-rules": "1.8.0-plusbeta",
"@microsoft/sp-webpart-workbench": "1.8.0-plusbeta",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@voitanos/jest-preset-spfx-react16": "^1.1.0",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "1.0.0",
"gulp-stylelint": "^8.0.0",
"jest": "^23.6.0",
"karma-junit-reporter": "^1.2.0",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0",
"stylelint-scss": "^3.5.4",
"webpack-bundle-analyzer": "^3.1.0"
}
}

View File

@ -0,0 +1,98 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.description {
border: 1px solid #a6a6a6;
padding: 10;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
padding-top: 10px;
height: 300px;
max-height: 320px;
overflow: auto;
}
.description:hover {
border-color: rgb( 51, 51, 51 );
}
.calendar {
.container {
max-width: 100%;
min-height: 400px;
height: 600px;
margin: 0px auto;
}
.eventTitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
border-style: solid;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,14 @@
import { IEventData } from '../../services/IEventData';
import { IPanelModelEnum} from './IPanelModeEnum';
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEventProps {
event: IEventData;
panelMode: IPanelModelEnum;
onDissmissPanel: (refresh:boolean) => void;
showPanel: boolean;
startDate?: Date;
endDate?: Date;
context:WebPartContext;
siteUrl: string;
listId:string;
}

View File

@ -0,0 +1,28 @@
import { IEventData } from '../../services/IEventData';
import { IUserPermissions } from '../../services/IUserPermissions';
import { DayOfWeek} from 'office-ui-fabric-react/lib/DatePicker';
import { IDropdownOption } from 'office-ui-fabric-react/';
export interface IEventState {
showPanel: boolean;
eventData:IEventData;
firstDayOfWeek?: DayOfWeek;
startSelectedHour: IDropdownOption ;
startSelectedMin: IDropdownOption ;
endSelectedHour: IDropdownOption ;
endSelectedMin: IDropdownOption ;
startDate?: Date;
endDate?: Date;
editorState?: any;
selectedUsers: string[];
locationLatitude: number;
locationLongitude: number;
errorMessage?:string;
hasError?:boolean;
disableButton?: boolean;
isSaving?:boolean;
isDeleting?:boolean;
displayDialog:boolean;
userPermissions?: IUserPermissions;
isloading:boolean;
siteRegionalSettings: any;
}

View File

@ -0,0 +1,5 @@
export enum IPanelModelEnum {
add=1,
edit=2,
delete=3
}

View File

@ -0,0 +1,769 @@
import * as React from 'react';
import styles from './Event.module.scss';
import * as strings from 'CalendarWebPartStrings';
import { IEventProps } from './IEventProps';
import { IEventState } from './IEventState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { PeoplePicker, PrincipalType } from "@pnp/spfx-controls-react/lib/PeoplePicker";
import {
Panel,
PanelType,
TextField,
Label,
extendComponent
} from 'office-ui-fabric-react';
import { EnvironmentType } from '@microsoft/sp-core-library';
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import { IEventData } from '../../services/IEventData';
import { IUserPermissions } from '../../services/IUserPermissions';
import {
DatePicker,
DayOfWeek,
IDatePickerStrings,
Dropdown,
DropdownMenuItemType,
IDropdownStyles,
IDropdownOption,
DefaultButton,
PrimaryButton,
IPersonaProps,
MessageBar,
MessageBarType,
Spinner,
SpinnerSize,
Dialog,
DialogType,
DialogFooter,
Toggle
}
from 'office-ui-fabric-react';
import { addMonths, addYears } from 'office-ui-fabric-react/lib/utilities/dateMath/DateMath';
import { _ComponentBaseKillSwitches } from '@microsoft/sp-component-base';
import { IPanelModelEnum } from './IPanelModeEnum';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import spservices from '../../services/spservices';
import { Map, ICoordinates, MapType } from "@pnp/spfx-controls-react/lib/Map";
const today: Date = new Date(Date.now());
const DayPickerStrings: IDatePickerStrings = {
months: [strings.January, strings.February, strings.March, strings.April, strings.May, strings.June, strings.July, strings.August, strings.September, strings.October, strings.November, strings.Dezember],
shortMonths: [strings.Jan, strings.Feb, strings.Mar, strings.Apr, strings.May, strings.Jun, strings.Jul, strings.Aug, strings.Sep, strings.Oct, strings.Nov, strings.Dez],
days: [strings.Sunday, strings.Monday, strings.Tuesday, strings.Wednesday, strings.Thursday, strings.Friday, strings.Saturday],
shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Tursday, strings.ShortDay_Friday, strings.ShortDay_Saunday],
goToToday: strings.GoToDay,
prevMonthAriaLabel: strings.PrevMonth,
nextMonthAriaLabel: strings.NextMonth,
prevYearAriaLabel: strings.PrevYear,
nextYearAriaLabel: strings.NextYear,
closeButtonAriaLabel: strings.CloseDate,
isRequiredErrorMessage: strings.IsRequired,
invalidInputErrorMessage: strings.InvalidDateFormat,
};
export class Event extends React.Component<IEventProps, IEventState> {
private spService: spservices = null;
private attendees: IPersonaProps[] = [];
private latitude: number = 41.1931819;
private longitude: number = -8.4897452;
private categoryDropdownOption: IDropdownOption[] = [];
public constructor(props) {
super(props);
/* geolocation is available */
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition((position) => {
this.latitude = position.coords.latitude;
this.longitude = position.coords.longitude;
});
} else {
/* geolocation IS NOT available */
console.log('browser Geolocation is not available');
}
// Initialize Map coordinates
console.log('ini', this.latitude, this.longitude);
this.state = {
showPanel: false,
eventData: this.props.event,
startSelectedHour: { key: '09', text: '00' },
startSelectedMin: { key: '00', text: '00' },
endSelectedHour: { key: '18', text: '00' },
endSelectedMin: { key: '00', text: '00' },
editorState: EditorState.createEmpty(),
selectedUsers: [],
locationLatitude: this.latitude,
locationLongitude: this.longitude,
hasError: false,
errorMessage: '',
disableButton: true,
isSaving: false,
displayDialog: false,
isloading: false,
siteRegionalSettings: undefined,
userPermissions: { hasPermissionAdd: false, hasPermissionDelete: false, hasPermissionEdit: false, hasPermissionView: false },
};
// local copia of props
this.onStartChangeHour = this.onStartChangeHour.bind(this);
this.onStartChangeMin = this.onStartChangeMin.bind(this);
this.onEndChangeHour = this.onEndChangeHour.bind(this);
this.onEndChangeMin = this.onEndChangeMin.bind(this);
this.onEditorStateChange = this.onEditorStateChange.bind(this);
this.onRenderFooterContent = this.onRenderFooterContent.bind(this);
this.onSave = this.onSave.bind(this);
this.onSelectDateEnd = this.onSelectDateEnd.bind(this);
this.onSelectDateStart = this.onSelectDateStart.bind(this);
this.onUpdateCoordinates = this.onUpdateCoordinates.bind(this);
this.onGetErrorMessageTitle = this.onGetErrorMessageTitle.bind(this);
this.getPeoplePickerItems = this.getPeoplePickerItems.bind(this);
this.hidePanel = this.hidePanel.bind(this);
this.onDelete = this.onDelete.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.confirmDelete = this.confirmDelete.bind(this);
this.onAllDayEventChange = this.onAllDayEventChange.bind(this);
this.onCategoryChanged = this.onCategoryChanged.bind(this);
this.spService = new spservices(this.props.context);
}
/**
* Hide Panel
*
* @private
* @memberof Event
*/
private hidePanel() {
this.props.onDissmissPanel(false);
}
/**
* Save Event to a list
* @private
* @memberof Event
*/
private async onSave() {
let eventData: IEventData = this.state.eventData;
// All Day event ?
const startDate = `${moment(this.state.startDate).format('YYYY/MM/DD')}`;
const startTime = `${this.state.startSelectedHour.key}:${this.state.startSelectedMin.key}`;
const startDateTime = `${startDate} ${startTime}`;
const start = moment(startDateTime, 'YYYY/MM/DD HH:mm').toLocaleString();
eventData.start = new Date(start);
// End Date
const endDate = `${moment(this.state.endDate).format('YYYY/MM/DD')}`;
const endTime = `${this.state.endSelectedHour.key}:${this.state.endSelectedMin.key}`;
const endDateTime = `${endDate} ${endTime}`;
const end = moment(endDateTime, 'YYYY/MM/DD HH:mm').toLocaleString();
eventData.end = new Date(end);
debugger;
// get Geolocation
eventData.geolocation = { Latitude: this.latitude, Longitude: this.longitude };
const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude);
eventData.location = locationInfo ? locationInfo.display_name : 'N/A';
console.log('beforeupd',eventData.geolocation);
// get Attendees
if (!eventData.attendes) { //vinitialize if no attendees
eventData.attendes = [];
}
// Get Descript from RichText Compoment
eventData.Description = draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()));
try {
for (const user of this.attendees) {
const userInfo: any= await this.spService.getUserByLoginName(user.id, this.props.siteUrl);
eventData.attendes.push(parseInt(userInfo.Id));
}
this.setState({ isSaving: true });
switch (this.props.panelMode) {
case IPanelModelEnum.edit:
await this.spService.updateEvent(eventData, this.props.siteUrl, this.props.listId);
break;
case IPanelModelEnum.add:
await this.spService.addEvent(eventData, this.props.siteUrl, this.props.listId);
break;
default:
break;
}
this.setState({ isSaving: false });
this.props.onDissmissPanel(true);
} catch (error) {
this.setState({ hasError: true, errorMessage: error.message, isSaving: false });
}
}
/**
*
* @param {*} error
* @param {*} errorInfo
* @memberof Event
*/
public componentDidCatch(error: any, errorInfo: any) {
this.setState({ hasError: true, errorMessage: errorInfo.componentStack });
}
/**
*
*
* @memberof Event
*/
public async componentDidMount() {
this.setState({ isloading: true });
let editorState:EditorState;
// Load Regional Settings
const siteRegionalSettigns = await this.spService.getSiteRegionalSettingsTimeZone(this.props.siteUrl);
// chaeck User list Permissions
const userListPermissions: IUserPermissions = await this.spService.getUserPermissions(this.props.siteUrl, this.props.listId);
// Load Categories
this.categoryDropdownOption = await this.spService.getChoiceFieldOptions(this.props.siteUrl, this.props.listId, 'Category');
// Edit Mode ?
if (this.props.panelMode == IPanelModelEnum.edit && this.props.event) {
// Get hours of event
const startHour = moment(this.props.event.start).format('HH').toString();
const startMin = moment(this.props.event.start).format('mm').toString();
const endHour = moment(this.props.event.end).format('HH').toString();
const endMin = moment(this.props.event.end).format('mm').toString();
// Get Descript and covert to RichText Control
const html = this.props.event.Description;
const contentBlock = htmlToDraft(html);
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
editorState = EditorState.createWithContent(contentState);
}
// testa attendees
const attendees = this.props.event.attendes;
let selectedUsers: string[] = [];
if (attendees && attendees.length > 0) {
for (const userId of attendees) {
let user: any = await this.spService.getUserById(userId, this.props.siteUrl);
if (user) {
selectedUsers.push(user.UserPrincipalName);
}
}
}
// Has geolocation ?
this.latitude = this.props.event.geolocation && this.props.event.geolocation.Latitude ? this.props.event.geolocation.Latitude : this.latitude;
this.longitude = this.props.event.geolocation && this.props.event.geolocation.Longitude ? this.props.event.geolocation.Longitude : this.longitude;
// Update Component Data
this.setState({
eventData: this.props.event,
startDate: this.props.event.start,
endDate: this.props.event.end,
startSelectedHour: { key: startHour, text: startHour },
startSelectedMin: { key: startMin, text: startMin },
endSelectedHour: { key: endHour, text: endHour },
endSelectedMin: { key: endMin, text: endMin },
editorState: editorState,
selectedUsers: selectedUsers,
userPermissions: userListPermissions,
isloading: false,
siteRegionalSettings: siteRegionalSettigns,
locationLatitude: this.latitude,
locationLongitude: this.longitude,
});
} else {
editorState = EditorState.createEmpty();
this.setState({
startDate: this.props.startDate ? this.props.startDate : new Date(),
endDate: this.props.endDate ? this.props.endDate : new Date(),
editorState: editorState,
userPermissions: userListPermissions,
isloading: false,
siteRegionalSettings: siteRegionalSettigns,
});
}
}
/**
*
* @memberof Event
*/
public componentWillMount() {
}
/**
* @private
* @memberof Event
*/
private onStartChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ startSelectedHour: item });
}
/**
* @private
* @memberof Event
*/
private onEndChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ endSelectedHour: item });
}
/**
* @private
* @memberof Event
*/
private onStartChangeMin = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ startSelectedMin: item });
}
/**
* @private
* @param {any[]} items
* @memberof Event
*/
private getPeoplePickerItems(items: any[]) {
this.attendees = [];
this.attendees = items;
}
/**
*
* @private
* @param {*} editorState
* @memberof Event
*/
private onEditorStateChange(editorState) {
this.setState({
editorState,
});
}
/**
*
* @private
* @param {string} value
* @returns {string}
* @memberof Event
*/
private onGetErrorMessageTitle(value: string): string {
let returnMessage: string = '';
if (value.length === 0) {
returnMessage = strings.EventTitleErrorMessage;
} else {
this.setState({ eventData: { ...this.state.eventData, title: value }, disableButton: false, errorMessage: '' });
}
return returnMessage;
}
/**
*
* @private
* @memberof Event
*/
private onEndChangeMin(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
ev.preventDefault();
this.setState({ endSelectedMin: item });
}
/**
*
*
* @private
* @param {React.FormEvent<HTMLDivElement>} ev
* @param {IDropdownOption} item
* @memberof Event
*/
private onCategoryChanged(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
ev.preventDefault();
this.setState({ eventData: { ...this.state.eventData, Category: item.text } });
}
/**
*
* @private
* @param {React.MouseEvent<HTMLDivElement>} event
* @memberof Event
*/
private onDelete(ev: React.MouseEvent<HTMLDivElement>) {
ev.preventDefault();
this.setState({ displayDialog: true });
}
/**
*
* @private
* @param {React.MouseEvent<HTMLDivElement>} event
* @memberof Event
*/
private closeDialog(ev: React.MouseEvent<HTMLDivElement>) {
ev.preventDefault();
this.setState({ displayDialog: false });
}
private async confirmDelete(ev: React.MouseEvent<HTMLDivElement>) {
ev.preventDefault();
try {
this.setState({ isDeleting: true });
switch (this.props.panelMode) {
case IPanelModelEnum.edit:
await this.spService.deleteEvent(this.state.eventData, this.props.siteUrl, this.props.listId);
break;
default:
break;
}
this.setState({ isDeleting: false });
this.props.onDissmissPanel(true);
} catch (error) {
this.setState({ hasError: true, errorMessage: error.message, isDeleting: false });
}
}
/**
* @private
* @returns
* @memberof Event
*/
private onRenderFooterContent() {
return (
<div >
<DefaultButton onClick={this.hidePanel} style={{ marginBottom: '15px', float: 'right' }}>
{strings.CancelButtonLabel}
</DefaultButton>
{
this.props.panelMode == IPanelModelEnum.edit && this.state.userPermissions.hasPermissionDelete && (
<DefaultButton onClick={this.onDelete} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}>
{strings.DeleteButtonLabel}
</DefaultButton>
)
}
{
(this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit) &&
<PrimaryButton disabled={this.state.disableButton} onClick={this.onSave} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}>
{strings.SaveButtonLabel}
</PrimaryButton>
}
{
this.state.isSaving &&
<Spinner size={SpinnerSize.medium} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }} />
}
</div>
);
}
/**
*
* @private
* @param {Date} newDate
* @memberof Event
*/
private onSelectDateStart(newDate: Date) {
this.setState({ startDate: newDate });
}
/**
* @private
* @param {Date} newDate
* @memberof Event
*/
private onSelectDateEnd(newDate: Date) {
this.setState({ endDate: newDate });
}
private onAllDayEventChange(ev: React.MouseEvent<HTMLElement>, checked: boolean) {
ev.preventDefault();
this.setState({ eventData: { ...this.state.eventData, allDayEvent: checked } });
}
/**
*
* @private
* @param {ICoordinates} coordinates
* @memberof Event
*/
private async onUpdateCoordinates(coordinates: ICoordinates) {
this.latitude = coordinates.latitude;
this.longitude = coordinates.longitude;
console.log('upcoor',this.latitude + ' ' + this.longitude);
const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude);
this.setState({ eventData: { ...this.state.eventData, location: locationInfo.display_name } });
}
public render(): React.ReactElement<IEventProps> {
console.log(this.state.locationLatitude + '-' + this.state.locationLongitude);
const { editorState } = this.state;
return (
<div>
<Panel
isOpen={this.props.showPanel}
onDismiss={this.hidePanel}
type={PanelType.medium}
headerText={strings.EventPanelTitle}
isFooterAtBottom={true}
onRenderFooterContent={this.onRenderFooterContent}
>
<div style={{ width: '100%' }}>
{
this.state.hasError &&
<MessageBar messageBarType={MessageBarType.error}>
{this.state.errorMessage}
</MessageBar>
}
{
this.state.isloading && (
<Spinner size={SpinnerSize.large} />
)
}
{
!this.state.isloading &&
<div>
<div>
<TextField
label={strings.EventTitleLabel}
value={this.state.eventData ? this.state.eventData.title : ''}
onGetErrorMessage={this.onGetErrorMessageTitle}
deferredValidationTime={500}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div>
<Dropdown
label={strings.CategoryLabel}
selectedKey={this.state.eventData && this.state.eventData.Category ? this.state.eventData.Category : ''}
onChange={this.onCategoryChanged}
options={this.categoryDropdownOption}
placeholder={strings.CategoryPlaceHolder}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<DatePicker
isRequired={false}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
allowTextInput={true}
value={this.state.startDate}
label={strings.StartDateLabel}
onSelectDate={this.onSelectDateStart}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<Dropdown
selectedKey={this.state.startSelectedHour.key}
onChange={this.onStartChangeHour}
label={strings.StartHourLabel}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '01', text: '01' },
{ key: '02', text: '02' },
{ key: '03', text: '03' },
{ key: '04', text: '04' },
{ key: '05', text: '05' },
{ key: '06', text: '06' },
{ key: '07', text: '07' },
{ key: '08', text: '08' },
{ key: '09', text: '09' },
{ key: '10', text: '10' },
{ key: '11', text: '11' },
{ key: '12', text: '12' },
{ key: '13', text: '13' },
{ key: '14', text: '14' },
{ key: '15', text: '15' },
{ key: '16', text: '16' },
{ key: '17', text: '17' },
{ key: '18', text: '18' },
{ key: '19', text: '19' },
{ key: '20', text: '20' },
{ key: '21', text: '21' },
{ key: '22', text: '22' },
{ key: '23', text: '23' }
]}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', }}>
<Dropdown
label={strings.StartMinLabel}
selectedKey={this.state.startSelectedMin.key}
onChange={this.onStartChangeMin}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '05', text: '05' },
{ key: '10', text: '10' },
{ key: '15', text: '15' },
{ key: '20', text: '20' },
{ key: '25', text: '25' },
{ key: '30', text: '30' },
{ key: '35', text: '35' },
{ key: '40', text: '40' },
{ key: '45', text: '45' },
{ key: '50', text: '50' },
{ key: '55', text: '55' }
]}
/>
</div>
<br />
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<DatePicker
isRequired={false}
strings={DayPickerStrings}
placeholder={strings.EndDatePlaceHolder}
ariaLabel={strings.EndDatePlaceHolder}
allowTextInput={true}
value={this.state.endDate}
label={strings.EndDateLabel}
onSelectDate={this.onSelectDateEnd}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<Dropdown
selectedKey={this.state.endSelectedHour.key}
onChange={this.onEndChangeHour}
label={strings.EndHourLabel}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '01', text: '01' },
{ key: '02', text: '02' },
{ key: '03', text: '03' },
{ key: '04', text: '04' },
{ key: '05', text: '05' },
{ key: '06', text: '06' },
{ key: '07', text: '07' },
{ key: '08', text: '08' },
{ key: '09', text: '09' },
{ key: '10', text: '10' },
{ key: '11', text: '11' },
{ key: '12', text: '12' },
{ key: '13', text: '13' },
{ key: '14', text: '14' },
{ key: '15', text: '15' },
{ key: '16', text: '16' },
{ key: '17', text: '17' },
{ key: '18', text: '18' },
{ key: '19', text: '19' },
{ key: '20', text: '20' },
{ key: '21', text: '21' },
{ key: '22', text: '22' },
{ key: '23', text: '23' }
]}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', }}>
<Dropdown
label={strings.EndMinLabel}
selectedKey={this.state.endSelectedMin.key}
onChange={this.onEndChangeMin}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '05', text: '05' },
{ key: '10', text: '10' },
{ key: '15', text: '15' },
{ key: '20', text: '20' },
{ key: '25', text: '25' },
{ key: '30', text: '30' },
{ key: '35', text: '35' },
{ key: '40', text: '40' },
{ key: '45', text: '45' },
{ key: '50', text: '50' },
{ key: '55', text: '55' },
{ key: '59', text: '59' }
]}
/>
</div>
<Label>{this.state.siteRegionalSettings ? this.state.siteRegionalSettings.Description : ''}</Label>
<br />
<Label>Event Description</Label>
<div className={styles.description}>
<Editor
editorState={editorState}
onEditorStateChange={this.onEditorStateChange}
ReadOnly={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div>
<PeoplePicker
webAbsoluteUrl={this.props.siteUrl}
context={this.props.context}
titleText={strings.AttendeesLabel}
principalTypes={[PrincipalType.User]}
resolveDelay={1000}
showtooltip={true}
selectedItems={this.getPeoplePickerItems}
personSelectionLimit={10}
defaultSelectedUsers={this.state.selectedUsers}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div>
<TextField
value={this.state.eventData && this.state.eventData.location ? this.state.eventData.location : ''}
label={strings.LocationTextLabel}
readOnly
multiline />
</div>
<div>
<Map titleText={strings.LocationLabel}
coordinates={{ latitude: this.state.locationLatitude, longitude: this.state.locationLongitude }}
enableSearch={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? true : false}
onUpdateCoordinates={this.onUpdateCoordinates}
/>
</div>
</div>
}
</div>
{
this.state.displayDialog &&
<div>
<Dialog
hidden={!this.state.displayDialog}
dialogContentProps={{
type: DialogType.normal,
title: strings.DialogConfirmDeleteTitle,
showCloseButton: false
}}
modalProps={{
isBlocking: true,
styles: { main: { maxWidth: 450 } }
}}
>
<Label >{strings.ConfirmeDeleteMessage}</Label>
{
this.state.isDeleting &&
<Spinner size={SpinnerSize.medium} ariaLabel={strings.SpinnerDeletingLabel} />
}
<DialogFooter>
<PrimaryButton onClick={this.confirmDelete} text={strings.DialogConfirmDeleteLabel} disabled={this.state.isDeleting} />
<DefaultButton onClick={this.closeDialog} text={strings.DialogCloseButtonLabel} />
</DialogFooter>
</Dialog>
</div>
}
</Panel>
</div>
);
}
}

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,17 @@
export interface IEventData {
id?:number;
title: string;
Description?: any;
location?:string;
start: Date;
end: Date;
color?:string;
ownerInitial?: string;
ownerPhoto?:string;
ownerEmail?:string;
ownerName?:string;
allDayEvent?: boolean;
attendes?: number[];
geolocation?: {Longitude:number, Latitude: number};
Category?: string;
}

View File

@ -0,0 +1,4 @@
export interface IList {
ID: string;
Title: string;
}

View File

@ -0,0 +1,5 @@
export interface IListFields {
Title: String;
InternalName: string;
TypeAsString: string;
}

View File

@ -0,0 +1,6 @@
export interface IUserPermissions {
hasPermissionAdd: boolean;
hasPermissionEdit: boolean;
hasPermissionDelete: boolean;
hasPermissionView: boolean;
}

View File

@ -0,0 +1,470 @@
// João Mendes
// March 2019
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { sp, Fields, Web, SearchResults, Field, PermissionKind, RegionalSettings } from '@pnp/sp';
import { graph, } from "@pnp/graph";
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions, HttpClient, MSGraphClient } from '@microsoft/sp-http';
import * as $ from 'jquery';
import { IEventData } from './IEventData';
import { registerDefaultFontFaces } from "@uifabric/styling";
import { EventArgs } from "@microsoft/sp-core-library";
import * as moment from 'moment';
import { SiteUser } from "@pnp/sp/src/siteusers";
import { IUserPermissions } from './IUserPermissions';
import { dateAdd } from "@pnp/common";
const ADMIN_ROLETEMPLATE_ID = "62e90394-69f5-4237-9190-012177145e10"; // Global Admin TemplateRoleId
// Class Services
export default class spservices {
private graphClient: MSGraphClient = null;
constructor(private context: WebPartContext) {
// Setuo Context to PnPjs and MSGraph
sp.setup({
spfxContext: this.context
});
graph.setup({
spfxContext: this.context
});
// Init
this.onInit();
}
// OnInit Function
private async onInit() {
//this.appCatalogUrl = await this.getAppCatalogUrl();
}
/**
*
* @private
* @param {string} siteUrl
* @returns {Promise<number>}
* @memberof spservices
*/
private async getSiteTimeZoneHoursToUtc(siteUrl: string): Promise<number> {
let numberHours: number = 0;
let siteTimeZoneHoursToUTC: any;
let siteTimeZoneBias: number;
let siteTimeZoneDaylightBias: number;
let currentDateTimeOffSet: number = new Date().getTimezoneOffset() / 60;
try {
const siteRegionalSettings: any = await this.getSiteRegionalSettingsTimeZone(siteUrl);
// Calculate hour to current site
siteTimeZoneBias = siteRegionalSettings.Information.Bias;
siteTimeZoneDaylightBias = siteRegionalSettings.Information.DaylightBias;
// Formula to calculate the number of hours need to get UTC Date.
numberHours = (siteTimeZoneBias / 60) + (siteTimeZoneDaylightBias / 60) - currentDateTimeOffSet;
}
catch (error) {
return Promise.reject(error);
}
return numberHours;
}
/**
*
* @param {IEventData} newEvent
* @param {string} siteUrl
* @param {string} listId
* @returns
* @memberof spservices
*/
public async addEvent(newEvent: IEventData, siteUrl: string, listId: string) {
let results = null;
try {
const web = new Web(siteUrl);
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.add({
Title: newEvent.title,
Description: newEvent.Description,
Geolocation: newEvent.geolocation,
ParticipantsPickerId: { results: newEvent.attendes },
EventDate: new Date(moment(newEvent.start).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
EndDate: new Date(moment(newEvent.end).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
Location: newEvent.location,
fAllDayEvent: false,
fRecurrence: false,
Category: newEvent.Category,
});
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {IEventData} newEvent
* @param {string} siteUrl
* @param {string} listId
* @returns
* @memberof spservices
*/
public async updateEvent(updateEvent: IEventData, siteUrl: string, listId: string) {
let results = null;
try {
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl);
const web = new Web(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.getById(updateEvent.id).update({
Title: updateEvent.title,
Description: updateEvent.Description,
Geolocation: updateEvent.geolocation,
ParticipantsPickerId: { results: updateEvent.attendes },
EventDate: new Date(moment(updateEvent.start).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
EndDate: new Date(moment(updateEvent.end).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
Location: updateEvent.location,
fAllDayEvent: false,
fRecurrence: false,
Category: updateEvent.Category,
});
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {IEventData} event
* @param {string} siteUrl
* @param {string} listId
* @returns
* @memberof spservices
*/
public async deleteEvent(event: IEventData, siteUrl: string, listId: string) {
let results = null;
try {
const web = new Web(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.getById(event.id).delete();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {number} userId
* @param {string} siteUrl
* @returns {Promise<SiteUser>}
* @memberof spservices
*/
public async getUserById(userId: number, siteUrl: string): Promise<SiteUser> {
let results: SiteUser = null;
if (!userId && !siteUrl) {
return null;
}
try {
const web = new Web(siteUrl);
results = await web.siteUsers.getById(userId).get();
//results = await web.siteUsers.getByLoginName(userId).get();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
*
* @param {string} loginName
* @param {string} siteUrl
* @returns {Promise<SiteUser>}
* @memberof spservices
*/
public async getUserByLoginName(loginName: string, siteUrl: string): Promise<SiteUser> {
let results: SiteUser = null;
if (!loginName && !siteUrl) {
return null;
}
try {
const web = new Web(siteUrl);
await web.ensureUser(loginName);
results = await web.siteUsers.getByLoginName(loginName).get();
//results = await web.siteUsers.getByLoginName(userId).get();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {string} loginName
* @returns
* @memberof spservices
*/
public async getUserProfilePictureUrl(loginName: string) {
let results: any = null;
try {
results = await sp.profiles.getPropertiesFor(loginName);
} catch (error) {
results = null;
}
return results.PictureUrl;
}
/**
*
* @param {string} siteUrl
* @param {string} listId
* @returns {Promise<IUserPermissions>}
* @memberof spservices
*/
public async getUserPermissions(siteUrl: string, listId: string): Promise<IUserPermissions> {
let hasPermissionAdd: boolean = false;
let hasPermissionEdit: boolean = false;
let hasPermissionDelete: boolean = false;
let hasPermissionView: boolean = false;
let userPermissions: IUserPermissions = undefined;
try {
const web = new Web(siteUrl);
hasPermissionAdd = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.AddListItems);
hasPermissionEdit = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.EditListItems);
hasPermissionDelete = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.DeleteListItems);
hasPermissionView = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.ViewListItems);
userPermissions = { hasPermissionAdd: hasPermissionAdd, hasPermissionEdit: hasPermissionEdit, hasPermissionDelete: hasPermissionDelete, hasPermissionView: hasPermissionView };
} catch (error) {
return Promise.reject(error);
}
return userPermissions;
}
/**
*
* @param {string} siteUrl
* @returns
* @memberof spservices
*/
public async getSiteLists(siteUrl: string) {
let results: any[] = [];
if (!siteUrl) {
return [];
}
try {
const web = new Web(siteUrl);
results = await web.lists.select("Title", "ID").filter('BaseTemplate eq 106').get();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @private
* @returns
* @memberof spservices
*/
public async colorGenerate() {
var hexValues = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e"];
var newColor = "#";
for (var i = 0; i < 6; i++) {
var x = Math.round(Math.random() * 14);
var y = hexValues[x];
newColor += y;
}
return newColor;
}
/**
*
* @param {string} siteUrl
* @param {string} listId
* @param {string} fieldInternalName
* @returns {Promise<{ key: string, text: string }[]>}
* @memberof spservices
*/
public async getChoiceFieldOptions(siteUrl: string, listId: string, fieldInternalName: string): Promise<{ key: string, text: string }[]> {
let fieldOptions: { key: string, text: string }[] = [];
try {
const web = new Web(siteUrl);
const results = await web.lists.getById(listId)
.fields
.getByInternalNameOrTitle(fieldInternalName)
.select("Title", "InternalName", "Choices")
.get();
if (results && results.Choices.length > 0) {
for (const option of results.Choices) {
fieldOptions.push({
key: option,
text: option
});
}
}
} catch (error) {
return Promise.reject(error);
}
return fieldOptions;
}
/**
*
* @param {string} siteUrl
* @param {string} listId
* @param {Date} eventStartDate
* @param {Date} eventEndDate
* @returns {Promise< IEventData[]>}
* @memberof spservices
*/
public async getEvents(siteUrl: string, listId: string, eventStartDate: Date, eventEndDate: Date): Promise<IEventData[]> {
let events: IEventData[] = [];
if (!siteUrl) {
return [];
}
try {
// Get Regional Settings TimeZone Hours to UTC
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl);
// Get Category Field Choices
const categoryDropdownOption = await this.getChoiceFieldOptions(siteUrl, listId, 'Category');
let categoryColor: { category: string, color: string }[] = [];
for (const cat of categoryDropdownOption) {
categoryColor.push({ category: cat.text, color: await this.colorGenerate() });
}
const web = new Web(siteUrl);
const results = await web.lists.getById(listId).renderListDataAsStream(
{
DatesInUtc: true,
ViewXml: `<View><ViewFields><FieldRef Name='Author'/><FieldRef Name='Category'/><FieldRef Name='Description'/><FieldRef Name='ParticipantsPicker'/><FieldRef Name='Geolocation'/><FieldRef Name='ID'/><FieldRef Name='EndDate'/><FieldRef Name='EventDate'/><FieldRef Name='ID'/><FieldRef Name='Location'/><FieldRef Name='Title'/><FieldRef Name='fAllDayEvent'/></ViewFields>
<Query>
<Where>
<And>
<And>
<Geq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventStartDate).format('YYYY-MM-DD')}</Value>
</Geq>
<Leq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventEndDate).format('YYYY-MM-DD')}</Value>
</Leq>
</And>
<Eq>
<FieldRef Name='fRecurrence' />
<Value Type='Recurrence'>0</Value>
</Eq>
</And>
</Where>
</Query>
<RowLimit Paged=\"FALSE\">2000</RowLimit>
</View>`
}
);
if (results && results.Row.length > 0) {
for (const event of results.Row) {
const initialsArray: string[] = event.Author[0].title.split(' ');
const initials: string = initialsArray[0].charAt(0) + initialsArray[initialsArray.length - 1].charAt(0);
const userPictureUrl = await this.getUserProfilePictureUrl(`i:0#.f|membership|${event.Author[0].email}`);
const attendees: number[] = [];
const first: number = event.Geolocation.indexOf('(') + 1;
const last: number = event.Geolocation.indexOf(')');
const geo = event.Geolocation.substring(first, last);
const geolocation = geo.split(' ');
const CategoryColorValue: any[] = categoryColor.filter((value) => {
return value.category == event.Category;
});
for (const attendee of event.ParticipantsPicker) {
attendees.push(parseInt(attendee.id));
}
events.push({
id: event.ID,
title: event.Title,
Description: event.Description,
// start: moment(event.EventDate).utc().toDate().setUTCMinutes(this.siteTimeZoneOffSet),
start: new Date(moment(event.EventDate).subtract(siteTimeZoneHoursToUTC, 'hour').toISOString()),
// end: new Date(moment(event.EndDate).toLocaleString()),
end: new Date(moment(event.EndDate).subtract(siteTimeZoneHoursToUTC, 'hour').toISOString()),
location: event.Location,
ownerEmail: event.Author[0].email,
ownerPhoto: userPictureUrl ?
`https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${event.Author[0].email}&UA=0&size=HR96x96` : '',
ownerInitial: initials,
// color: await this.colorGenerate(),
color: CategoryColorValue.length > 0 ? CategoryColorValue[0].color : await this.colorGenerate,
ownerName: event.Author[0].title,
attendes: attendees,
allDayEvent: false,
geolocation: { Longitude: parseFloat(geolocation[0]), Latitude: parseFloat(geolocation[1]) },
Category: event.Category
});
}
}
// Return Data
return events;
} catch (error) {
console.dir(error);
return Promise.reject(error);
}
}
/**
*
* @private
* @param {string} siteUrl
* @returns
* @memberof spservices
*/
public async getSiteRegionalSettingsTimeZone(siteUrl: string) {
let regionalSettings: RegionalSettings;
try {
const web = new Web(siteUrl);
regionalSettings = await web.regionalSettings.timeZone.get();
} catch (error) {
return Promise.reject(error);
}
return regionalSettings;
}
/**
* @param {string} webUrl
* @param {string} siteDesignId
* @returns
* @memberof spservices
*/
public async getGeoLactionName(latitude: number, longitude: number) {
try {
const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`;
const results = await $.ajax({
url: apiUrl,
type: 'GET',
dataType: 'json',
headers: {
'content-type': 'application/json;charset=utf-8',
'accept': 'application/json;odata=nometadata',
}
});
if (results) {
return results;
}
} catch (error) {
return Promise.reject(error);
}
}
}

View File

@ -0,0 +1,39 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "24353da0-cf7a-4ca8-85a2-c9cc91b5b865",
"alias": "CalendarWebPart",
"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"
],
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": {
"default": "Other"
},
"title": {
"default": "Calendar"
},
"description": {
"default": "Calendar Events"
},
"officeFabricIconFontName": "Calendar",
"properties": {
"title": "Calendar",
"siteUrl": "",
"list": "",
"eventStartDate": "",
"eventEndDate": ""
}
}
]
}

View File

@ -0,0 +1,294 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart, PropertyPaneHorizontalRule } from '@microsoft/sp-webpart-base';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
PropertyPaneLabel
} from '@microsoft/sp-property-pane';
import * as strings from 'CalendarWebPartStrings';
import Calendar from './components/Calendar';
import { ICalendarProps } from './components/ICalendarProps';
import { PropertyFieldDateTimePicker, DateConvention, TimeConvention, IDateTimeFieldValue } from '@pnp/spfx-property-controls/lib/PropertyFieldDateTimePicker';
export interface ICalendarWebPartProps {
title: string;
siteUrl: string;
list: string;
eventStartDate: IDateTimeFieldValue ;
eventEndDate: IDateTimeFieldValue;
errorMessage: string;
}
import spservices from '../../services/spservices';
import * as moment from 'moment';
import { format } from '@uifabric/utilities';
export default class CalendarWebPart extends BaseClientSideWebPart<ICalendarWebPartProps> {
private lists: IPropertyPaneDropdownOption[] = [];
private listsDropdownDisabled: boolean = true;
private spService: spservices = null;
private errorMessage: string;
public constructor() {
super();
}
public render(): void {
const element: React.ReactElement<ICalendarProps> = React.createElement(
Calendar,
{
title: this.properties.title,
siteUrl: this.properties.siteUrl,
list: this.properties.list,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.title = value;
},
context: this.context,
eventStartDate: this.properties.eventStartDate,
eventEndDate: this.properties.eventEndDate,
}
);
ReactDom.render(element, this.domElement);
}
// onInit
public async onInit(): Promise<void> {
this.spService = new spservices(this.context);
this.properties.siteUrl = this.context.pageContext.site.absoluteUrl;
if (!this.properties.eventStartDate){
this.properties.eventStartDate = { value: moment().subtract(2,'years').startOf('month').toDate(), displayValue: moment().format('ddd MMM MM YYYY')};
}
if (!this.properties.eventEndDate){
this.properties.eventEndDate = { value: moment().add(20,'years').endOf('month').toDate(), displayValue: moment().format('ddd MMM MM YYYY')};
}
if (this.properties.siteUrl && !this.properties.list) {
const _lists = await this.loadLists();
this.lists = _lists;
this.properties.list = this.lists.length > 0 ? this.lists[0].key.toString() : '';
}
return Promise.resolve();
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
/**
*
* @protected
* @memberof CalendarWebPart
*/
protected async onPropertyPaneConfigurationStart() {
try {
if (this.properties.siteUrl) {
const _lists = await this.loadLists();
this.lists = _lists;
this.listsDropdownDisabled = false;
// await this.loadFields(this.properties.siteUrl);
this.context.propertyPane.refresh();
} else {
this.lists = [];
this.properties.list = '';
this.listsDropdownDisabled = false;
this.context.propertyPane.refresh();
}
} catch (error) {
}
}
/**
*
* @private
* @returns {Promise<IPropertyPaneDropdownOption[]>}
* @memberof CalendarWebPart
*/
private async loadLists(): Promise<IPropertyPaneDropdownOption[]> {
const _lists: IPropertyPaneDropdownOption[] = [];
try {
const results = await this.spService.getSiteLists(this.properties.siteUrl);
for (const list of results) {
_lists.push({ key: list.Id, text: list.Title });
}
} catch (error) {
this.errorMessage = `${error.message} - ${strings.PropPanelSiteUrlErrorMessage}` ;
this.context.propertyPane.refresh();
}
return _lists;
}
/**
*
* @private
* @param {string} date
* @returns
* @memberof CalendarWebPart
*/
private onEventStartDateValidation(date:string){
if (date && this.properties.eventEndDate.value){
if (moment(date).isAfter(moment(this.properties.eventEndDate.value))){
return strings.SartDateValidationMessage;
}
}
return '';
}
/**
*
* @private
* @param {string} date
* @returns
* @memberof CalendarWebPart
*/
private onEventEndDateValidation(date:string){
if (date && this.properties.eventEndDate.value){
if (moment(date).isBefore( moment(this.properties.eventStartDate.value))){
return strings.EnDateValidationMessage;
}
}
return '';
}
/**
*
* @private
* @param {string} value
* @returns {Promise<string>}
* @memberof CalendarWebPart
*/
private onSiteUrlGetErrorMessage(value: string) {
let returnValue: string = '';
if (value) {
returnValue = '';
} else {
const previousList: string = this.properties.list;
const previousSiteUrl: string = this.properties.siteUrl;
// reset selected item
this.properties.list = undefined;
this.properties.siteUrl = undefined;
this.lists = [];
this.listsDropdownDisabled = true;
this.onPropertyPaneFieldChanged('list', previousList, this.properties.list);
this.onPropertyPaneFieldChanged('siteUrl', previousSiteUrl, this.properties.siteUrl);
this.context.propertyPane.refresh();
}
return returnValue;
}
/**
*
* @protected
* @param {string} propertyPath
* @param {string} oldValue
* @param {string} newValue
* @memberof CalendarWebPart
*/
protected async onPropertyPaneFieldChanged(propertyPath: string, oldValue: string, newValue: string) {
try {
// reset any error
this.properties.errorMessage = undefined;
this.errorMessage = undefined;
this.context.propertyPane.refresh();
if (propertyPath === 'siteUrl' && newValue) {
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
const _oldValue = this.properties.list;
this.onPropertyPaneFieldChanged('list', _oldValue, this.properties.list);
this.context.propertyPane.refresh();
const _lists = await this.loadLists();
this.lists = _lists;
this.listsDropdownDisabled = false;
this.properties.list = this.lists.length > 0 ? this.lists[0].key.toString() : undefined;
this.context.propertyPane.refresh();
this.render();
}
else {
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
} catch (error) {
this.errorMessage = `${error.message} - ${strings.PropPanelSiteUrlErrorMessage}` ;
this.context.propertyPane.refresh();
}
}
/**
*
* @protected
* @returns {IPropertyPaneConfiguration}
* @memberof CalendarWebPart
*/
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('siteUrl', {
label: strings.SiteUrlFieldLabel,
onGetErrorMessage: this.onSiteUrlGetErrorMessage.bind(this),
value: this.context.pageContext.site.absoluteUrl,
deferredValidationTime: 1200,
}),
PropertyPaneDropdown('list', {
label: strings.ListFieldLabel,
options: this.lists,
disabled: this.listsDropdownDisabled,
}),
PropertyPaneLabel('eventStartDate', {
text: strings.eventSelectDatesLabel
}),
PropertyFieldDateTimePicker('eventStartDate', {
label: 'From',
initialDate: this.properties.eventStartDate,
dateConvention: DateConvention.Date,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
onGetErrorMessage: this.onEventStartDateValidation,
deferredValidationTime: 0,
key: 'eventStartDateId'
}),
PropertyFieldDateTimePicker('eventEndDate', {
label: 'to',
initialDate: this.properties.eventEndDate,
dateConvention: DateConvention.Date,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
onGetErrorMessage: this.onEventEndDateValidation,
deferredValidationTime: 0,
key: 'eventEndDateId'
}),
PropertyPaneLabel('errorMessage', {
text: this.errorMessage,
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,157 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.Documentcard {
width: '100%';
height: '100%'
}
.previewEventIcon {
justify-content: 'center';
display: 'flex';
align-items: 'center';
font-size: 32px;
}
.locationIcon {
justify-content: 'center';
display: 'flex';
align-items: 'top';
font-size: 22px;
margin-top: 10px;
}
.location {
justify-content: 'center';
display: 'flex';
align-items: 'top';
}
.DocumentCardDetails{
background-color: #f2f2f2;
border-style: 'solid';
border-width: 1.3px;
border-color: #e6e6e6;
height: 40px;
justify-content: 'center';
display: 'flex';
align-items: 'top';
}
.eventStyle{
background-color: white;
border-radius: '0px';
opacity: 1;
color: black;
border-width: 1.4px;
border-style: 'solid';
display: 'block'
}
.DocumentCardTitle {
font-weight: 'bold';
height: '100%';
}
.DocumentCardTitleTime{
justify-content: 'center';
display: 'flex';
align-items: 'top';
margin-bottom: 6px;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-semibold;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
}
.calendar {
.container {
max-width: 100%;
min-height: 400px;
height: 600px;
margin: 0px auto;
}
.plainCard {
width: 300;
height: 286;
}
.eventTitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
border-style: solid
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,373 @@
import * as React from 'react';
import styles from './Calendar.module.scss';
import { ICalendarProps } from './ICalendarProps';
import { ICalendarState } from './ICalendarState';
import { escape } from '@microsoft/sp-lodash-subset';
import BigCalendar from 'react-big-calendar';
import * as moment from 'moment';
import * as strings from 'CalendarWebPartStrings';
import 'react-big-calendar/lib/css/react-big-calendar.css';
require('./calendar.css');
import {
IPersonaSharedProps,
Persona,
PersonaSize,
PersonaPresence,
HoverCard, IHoverCard, IPlainCardProps, HoverCardType, DefaultButton,
DocumentCard,
DocumentCardActivity,
DocumentCardDetails,
DocumentCardPreview,
DocumentCardTitle,
IDocumentCardPreviewProps,
IDocumentCardPreviewImage,
DocumentCardType,
Label,
ImageFit,
IDocumentCardLogoProps,
DocumentCardLogo,
DocumentCardImage,
Icon,
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
} from 'office-ui-fabric-react';
import { EnvironmentType } from '@microsoft/sp-core-library';
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from '@microsoft/sp-core-library';
import spservices from '../../../services/spservices';
import { stringIsNullOrEmpty } from '@pnp/common';
import { Event } from '../../../controls/Event/event';
import { IPanelModelEnum } from '../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../services/IEventData';
import { IUserPermissions } from './../../../services/IUserPermissions';
const localizer = BigCalendar.momentLocalizer(moment);
/**
* @export
* @class Calendar
* @extends {React.Component<ICalendarProps, ICalendarState>}
*/
export default class Calendar extends React.Component<ICalendarProps, ICalendarState> {
private spService: spservices = null;
private userListPermissions: IUserPermissions = undefined;
public constructor(props) {
super(props);
this.state = {
showDialog: false,
eventData: [],
selectedEvent: undefined,
isloading: true,
hasError: false,
errorMessage: '',
};
this.onDismissPanel = this.onDismissPanel.bind(this);
this.onSelectEvent = this.onSelectEvent.bind(this);
this.onSelectSlot = this.onSelectSlot.bind(this);
this.spService = new spservices(this.props.context);
moment.locale(this.props.context.pageContext.cultureInfo.currentUICultureName);
}
private onDocumentCardClick(ev: React.SyntheticEvent<HTMLElement, Event>) {
ev.preventDefault();
ev.stopPropagation();
}
/**
* @private
* @param {*} event
* @memberof Calendar
*/
private onSelectEvent(event: any) {
this.setState({ showDialog: true, selectedEvent: event, panelMode: IPanelModelEnum.edit });
}
/**
*
* @private
* @param {boolean} refresh
* @memberof Calendar
*/
private async onDismissPanel(refresh: boolean) {
this.setState({ showDialog: false });
if (refresh === true) {
this.setState({ isloading: true });
await this.loadEvents();
this.setState({ isloading: false });
}
}
/**
* @private
* @memberof Calendar
*/
private async loadEvents() {
try {
// Teste Properties
if (!this.props.list || !this.props.siteUrl || !this.props.eventStartDate.value || !this.props.eventEndDate.value) return;
this.userListPermissions = await this.spService.getUserPermissions(this.props.siteUrl, this.props.list);
const eventsData: IEventData[] = await this.spService.getEvents(escape(this.props.siteUrl), escape(this.props.list), this.props.eventStartDate.value, this.props.eventEndDate.value);
this.setState({ eventData: eventsData, hasError: false, errorMessage: "" });
} catch (error) {
this.setState({ hasError: true, errorMessage: error.message, isloading: false });
}
}
/**
* @memberof Calendar
*/
public async componentDidMount() {
this.setState({ isloading: true });
await this.loadEvents();
this.setState({ isloading: false });
}
/**
*
*
* @param {*} error
* @param {*} errorInfo
* @memberof Calendar
*/
public componentDidCatch(error: any, errorInfo: any) {
this.setState({ hasError: true, errorMessage: errorInfo.componentStack });
}
/**
*
*
* @param {ICalendarProps} prevProps
* @param {ICalendarState} prevState
* @memberof Calendar
*/
public async componentDidUpdate(prevProps: ICalendarProps, prevState: ICalendarState) {
if (!this.props.list || !this.props.siteUrl || !this.props.eventStartDate.value || !this.props.eventEndDate.value) return;
// Get Properties change
if (prevProps.list !== this.props.list || this.props.eventStartDate.value !== prevProps.eventStartDate.value || this.props.eventEndDate.value !== prevProps.eventEndDate.value) {
this.setState({ isloading: true });
await this.loadEvents();
this.setState({ isloading: false });
}
}
/**
* @private
* @param {*} { event }
* @returns
* @memberof Calendar
*/
private renderEvent({ event }) {
const previewEventIcon: IDocumentCardPreviewProps = {
previewImages: [
{
// previewImageSrc: event.ownerPhoto,
previewIconProps: { iconName: 'Calendar', styles: { root: { color: event.color } }, className: styles.previewEventIcon },
height: 43,
}
]
};
const EventInfo: IPersonaSharedProps = {
imageInitials: event.ownerInitial,
imageUrl: event.ownerPhoto,
text: event.title
};
/**
* @returns {JSX.Element}
*/
const onRenderPlainCard = (): JSX.Element => {
return (
<div className={styles.plainCard}>
<DocumentCard className={styles.Documentcard} >
<div>
<DocumentCardPreview {...previewEventIcon} />
</div>
<DocumentCardDetails>
<div className={styles.DocumentCardDetails}>
<DocumentCardTitle title={event.title} shouldTruncate={true} className={styles.DocumentCardTitle} styles={{ root: { color: event.color } }} />
</div>
{
moment(event.start).format('YYYY/MM/DD') !== moment(event.end).format('YYYY/MM/DD') ?
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('dddd')} - {moment(event.end).format('dddd')} </span>
:
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('dddd')} </span>
}
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('HH:mm')}H - {moment(event.end).format('HH:mm')}H</span>
<Icon iconName='MapPin' className={styles.locationIcon} style={{ color: event.color }} />
<DocumentCardTitle
title={`${event.location}`}
shouldTruncate={true}
showAsSecondaryTitle={true}
className={styles.location}
/>
<div style={{ marginTop: 20 }}>
<DocumentCardActivity
activity={strings.EventOwnerLabel}
people={[{ name: event.ownerName, profileImageSrc: event.ownerPhoto, initialsColor: event.color }]}
/>
</div>
</DocumentCardDetails>
</DocumentCard>
</div>
);
};
return (
<div style={{ height: 22 }}>
<HoverCard
cardDismissDelay={1000}
type={HoverCardType.plain}
plainCardProps={{ onRenderPlainCard: onRenderPlainCard }}
onCardHide={(): void => {
}}
>
<Persona
{...EventInfo}
size={PersonaSize.size24}
presence={PersonaPresence.none}
coinSize={22}
initialsColor={event.color}
/>
</HoverCard>
</div>
);
}
/**
*
*
* @private
* @memberof Calendar
*/
private onConfigure() {
// Context of the web part
this.props.context.propertyPane.open();
}
/**
* @param {*} { start, end }
* @memberof Calendar
*/
public async onSelectSlot({ start, end }) {
if (!this.userListPermissions.hasPermissionAdd) return;
this.setState({ showDialog: true, startDateSlot: start, endDateSlot: end, selectedEvent: undefined, panelMode: IPanelModelEnum.add });
}
/**
*
* @param {*} event
* @param {*} start
* @param {*} end
* @param {*} isSelected
* @returns {*}
* @memberof Calendar
*/
public eventStyleGetter(event, start, end, isSelected): any {
let style: any = {
backgroundColor: 'white',
borderRadius: '0px',
opacity: 1,
color: 'black',
borderWidth: '1.1px',
borderStyle: 'solid',
borderColor: event.color,
borderLeftWidth: '5px',
display: 'block'
};
return {
style: style
};
}
/**
*
* @returns {React.ReactElement<ICalendarProps>}
* @memberof Calendar
*/
public render(): React.ReactElement<ICalendarProps> {
return (
<div className={styles.calendar}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty} />
{
(!this.props.list || !this.props.eventStartDate.value || !this.props.eventEndDate.value) ?
<Placeholder iconName='Edit'
iconText={strings.WebpartConfigIconText}
description={strings.WebpartConfigDescription}
buttonLabel={strings.WebPartConfigButtonLabel}
hideButton={this.props.displayMode === DisplayMode.Read}
onConfigure={this.onConfigure.bind(this)} />
:
// test if has errors
this.state.hasError ?
<MessageBar messageBarType={MessageBarType.error}>
{this.state.errorMessage}
</MessageBar>
:
// show Calendar
// Test if is loading Events
<div>
{this.state.isloading ? <Spinner size={SpinnerSize.large} label={strings.LoadingEventsLabel} /> :
<div className={styles.container}>
<BigCalendar
localizer={localizer}
selectable
events={this.state.eventData}
startAccessor="start"
endAccessor="end"
eventPropGetter={this.eventStyleGetter}
onSelectSlot={this.onSelectSlot}
components={{
event: this.renderEvent
}}
onSelectEvent={this.onSelectEvent}
defaultDate={moment().startOf('day').toDate()}
messages={
{
'today': strings.todayLabel,
'previous': strings.previousLabel,
'next': strings.nextLabel,
'month': strings.monthLabel,
'week': strings.weekLabel,
'day': strings.dayLable,
'showMore': total => `+${total} ${strings.showMore}`
}
}
/>
</div>
}
</div>
}
{
this.state.showDialog &&
<Event
event={this.state.selectedEvent}
panelMode={this.state.panelMode}
onDissmissPanel={this.onDismissPanel}
showPanel={this.state.showDialog}
startDate={this.state.startDateSlot}
endDate={this.state.endDateSlot}
context={this.props.context}
siteUrl={this.props.siteUrl}
listId={this.props.list}
/>
}
</div>
);
}
}

View File

@ -0,0 +1,13 @@
import { DisplayMode } from '@microsoft/sp-core-library';
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { IDateTimeFieldValue } from '@pnp/spfx-property-controls/lib/PropertyFieldDateTimePicker';
export interface ICalendarProps {
title: string;
siteUrl: string;
list: string;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
context: WebPartContext;
eventStartDate: IDateTimeFieldValue;
eventEndDate: IDateTimeFieldValue;
}

View File

@ -0,0 +1,13 @@
import { IPanelModelEnum} from '../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../services/IEventData';
export interface ICalendarState {
showDialog: boolean;
eventData: IEventData[];
selectedEvent: IEventData;
panelMode?: IPanelModelEnum;
startDateSlot?: Date;
endDateSlot?:Date;
isloading: boolean;
hasError: boolean;
errorMessage: string;
}

View File

@ -0,0 +1,30 @@
import React from 'react';
import Toolbar from 'react-big-calendar/lib/Toolbar';
export default class CalendarToolbar extends Toolbar {
componentDidMount() {
const view = this.props.view;
console.log(view)
}
render() {
return (
<div>
<div className="rbc-btn-group">
<button type="button" onClick={() => this.navigate('TODAY')}>today</button>
<button type="button" onClick={() => this.navigate('PREV')}>back</button>
<button type="button" onClick={() => this.navigate('NEXT')}>next</button>
</div>
<div className="rbc-toolbar-label">{this.props.label}</div>
<div className="rbc-btn-group">
<button type="button" onClick={this.view.bind(null, 'month')}>Month</button>
<button type="button" onClick={this.view.bind(null, 'week')}>Week</button>
<button type="button" onClick={this.view.bind(null, 'day')}>Day</button>
<button type="button" onClick={this.view.bind(null, 'agenda')}>Agenda</button>
</div>
</div>
);
}
}
module.export = CalendarToolbar;

View File

@ -0,0 +1,92 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.rbc-selected-cell {
background-color: $ms-color-themeDark;
}
.rbc-event:hover {
border-width: 2px;
}
.rbc-today {
background-color: $ms-color-themeDark;
opacity: 0.7;
}
.rbc-off-range-bg {
background: #f4f4f4;
}
.rbc-toolbar button {
color: #373a3c;
display: inline-block;
margin: 0;
text-align: center;
vertical-align: middle;
background: none;
background-image: none;
border: 1px solid #ccc;
padding: .375rem 1rem;
/* border-radius: 4px; */
line-height: normal;
white-space: nowrap;
}
.rbc-toolbar button:active {
background-image: none;
/* box-shadow: inset 0 3px 5px rgba(0,0,0,.125); */
background-color: #f8f8f8;
border-color: #adadad;
}
.rbc-toolbar button {
color: #373a3c;
display: inline-block;
margin: 0;
text-align: center;
vertical-align: middle;
background: none;
background-image: none;
border: 1px solid #ccc;
padding: .375rem 1rem;
border-radius: 0px;
line-height: normal;
white-space: nowrap;
}
.rbc-toolbar button.rbc-active {
background-image: none;
background-color: $ms-color-themeDark;
/* background-color: #0075c7;*/
border-color: #f4f4f4;
color: white;
}
.rbc-toolbar button.rbc-active:focus, .rbc-toolbar button.rbc-active:hover, .rbc-toolbar button:active:focus, .rbc-toolbar button:active:hover {
color: #fff;
background-color: $ms-color-themeDark;
border-color: #f4f4f4;
}
.rbc-toolbar button:focus, .rbc-toolbar button:hover {
color: #f8f8f8;
background-color: $ms-color-themeDark;
border-color: #adadad;
}
.rbc-show-more {
background-color: hsla(0,0%,100%,.3);
z-index: 4;
font-weight: 700;
font-size: 85%;
height: auto;
line-height: normal;
white-space: nowrap;
}
.rbc-ellipsis, .rbc-event-label, .rbc-row-segment .rbc-event-content, .rbc-show-more {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -0,0 +1,96 @@
define([], function () {
return {
PropPanelSiteUrlErrorMessage:'Please verify if site url is valid',
HttpErrorMessage: "Error reading calendar events:",
CategoryPlaceHolder: "Please select category",
CategoryLabel: "Category",
EnDateValidationMessage: "start date is greater than end date",
SartDateValidationMessage: "start date is greater than end date",
eventSelectDatesLabel: "Show only the events within the following dates",
ConfirmeDeleteMessage: "Confirm delete event ?",
DialogConfirmDeleteTitle: " 'Delete Event'",
SpinnerDeletingLabel: "Deleting...",
DialogCloseButtonLabel: "Cancel",
DialogConfirmDeleteLabel: "Delete",
SaveButtonLabel: " Save",
DeleteButtonLabel: "Delete",
CancelButtonLabel: "Cancel",
LoadingEventsLabel: "Loading events...",
WebPartConfigButtonLabel: "Configure",
WebpartConfigDescription: "Please configure list calendar ",
WebpartConfigIconText: "Configure your Calendar Web Part",
EventOwnerLabel: "event owner",
InvalidDateFormat: "Invalid date format.",
IsRequired: "Field is required.",
CloseDate: "Close date picker",
NextYear: "Go to next year",
PrevYear: "Go to previous year",
NextMonth: "Go to next month",
PrevMonth: "Go to previous month",
GoToDay: "Go to today",
ShortDay_Saunday: "S",
ShortDay_Friday: "F",
ShortDay_Tursday: "T",
ShortDay_W: "W",
ShortDay_T: "T",
ShortDay_M: "M",
ShortDay_S: "S",
Saturday: "Saturday",
Friday: "Friday",
Thursday: "Thursday",
Wednesday: "Wednesday",
Tuesday: "Tuesday",
Monday: "Monday",
Sunday: "Sunday",
Jan:'Jan',
Feb:'Feb',
Mar:'Mar',
Apr:'Apr',
May:'May',
Jun:'Jun',
Jul:'Jul',
Aug:'Aug',
Sep:'Sep',
Oct:'Oct',
Nov:'Nov',
Dez:'Dez',
Dezember: "December",
November: " 'November'",
October: "October",
September: "September",
August: " 'August'",
July: "July",
June: "June",
May: "May",
April: "April",
March: "March",
February: "February",
January: "January",
LocationLabel: "Location search and Map",
LocationTextLabel: "Location",
AttendeesLabel: "Attendees",
EndMinLabel: "Min",
EndHourLabel: "Hour",
EndDateLabel: "End Date",
EndDatePlaceHolder: "Select a date...",
StartMinLabel: "Min",
StartHourLabel: "Hour",
StartDateLabel: "Start Date",
StartDatePlaceHolder: "Select a date...",
EventTitleErrorMessage: "Event Title is required.",
EventTitleLabel: "Event title",
EventPanelTitle: "Edit/Add Event",
"PropertyPaneDescription": "Calendar",
"BasicGroupName": "Properties",
SiteUrlFieldLabel: 'Site Url',
ListFieldLabel: 'Calendar List name',
weekLabel: 'Week',
dayLable: 'Day',
agenda: 'Agenda',
monthLabel: 'Month',
"todayLabel": 'Today',
"previousLabel": 'Previous',
"nextLabel": "Next",
"showMore": 'more'
}
});

View File

@ -0,0 +1,100 @@
declare interface ICalendarWebPartStrings {
HttpErrorMessage: string;
CategoryPlaceHolder: string;
CategoryLabel: string;
EnDateValidationMessage: string;
SartDateValidationMessage: string;
eventSelectDatesLabel: string;
ConfirmeDeleteMessage: string;
DialogConfirmDeleteTitle: string;
SpinnerDeletingLabel: string;
DialogCloseButtonLabel: string;
DialogConfirmDeleteLabel: string;
SaveButtonLabel: string;
DeleteButtonLabel: string;
CancelButtonLabel: string;
LoadingEventsLabel: string;
WebPartConfigButtonLabel: string;
WebpartConfigDescription: string;
WebpartConfigIconText: string;
EventOwnerLabel: string;
InvalidDateFormat: string;
IsRequired: string;
CloseDate: string;
NextYear: string;
PrevYear: string;
NextMonth: string;
PrevMonth: string;
GoToDay: string;
ShortDay_Saunday: string;
ShortDay_Friday: string;
ShortDay_Tursday: string;
ShortDay_W: string;
ShortDay_T: string;
ShortDay_M: string;
ShortDay_S: string;
Saturday: string;
Friday: string;
Thursday: string;
Wednesday: string;
Tuesday: string;
Monday: string;
Sunday: string;
Jan:string;
Feb:string;
Mar:string;
Apr:string;
May:string,
Jun:string;
Jul:string;
Aug:string;
Sep:string;
Oct:string;
Nov:string;
Dez:string;
Dezember: string;
November: string;
October: string;
September: string;
August: string;
July: string;
June: string;
May: string;
April: string;
March: string;
February: string;
January: string;
LocationLabel: string;
LocationTextLabel: string;
AttendeesLabel: string;
EndMinLabel: string;
EndHourLabel: string;
EndDateLabel: string;
EndDatePlaceHolder: string;
StartMinLabel: string;
StartHourLabel: string;
StartDateLabel: string;
StartDatePlaceHolder: string;
EventTitleErrorMessage: string;
EventTitleLabel: string;
EventPanelTitle: string;
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
SiteUrlFieldLabel: string;
ListFieldLabel: string;
monthLabel: string;
weekLabel: string;
dayLable: string;
agenda: string;
todayLabel: string;
previousLabel: string;
nextLabel: string;
showMore: string;
PropPanelSiteUrlErrorMessage: string;
}
declare module 'CalendarWebPartStrings' {
const strings: ICalendarWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,96 @@
define([], function() {
return {
PropPanelSiteUrlErrorMessage:'Por favor verifique se site url é valido.',
HttpErrorMessage: "Error reading calendar events:",
CategoryPlaceHolder: "Please select category",
CategoryLabel: "Category",
EnDateValidationMessage: "start date is greater than end date",
SartDateValidationMessage: "start date is greater than end date",
eventSelectDatesLabel: "Show only the events within the following dates",
ConfirmeDeleteMessage: "Confirm delete event ?",
DialogConfirmDeleteTitle: " 'Delete Event'",
SpinnerDeletingLabel: "Deleting...",
DialogCloseButtonLabel: "Cancel",
DialogConfirmDeleteLabel: "Delete",
SaveButtonLabel: " Save",
DeleteButtonLabel: "Delete",
CancelButtonLabel: "Cancel",
LoadingEventsLabel: "A carregar eventos...",
WebPartConfigButtonLabel: "Configurar",
WebpartConfigDescription: "Por favor configure configurar calendário ",
WebpartConfigIconText: "Configurar Web Part Calendarário",
EventOwnerLabel: "Organizador",
InvalidDateFormat: "Formato data inválido",
IsRequired: "Campo é obrigatório",
CloseDate: "Fechar date picker",
NextYear: "Ir para o próximo ano",
PrevYear: "Ir para o ano anterior",
NextMonth: "Ir para o próximo mês",
PrevMonth: "Ir para o mês anterior",
GoToDay: "Ir para dia de hoje",
ShortDay_Saunday: "S",
ShortDay_Friday: "S",
ShortDay_Tursday: "Q",
ShortDay_W: "Q",
ShortDay_T: "T",
ShortDay_M: "S",
ShortDay_S: "D",
Saturday: "Sábado",
Friday: "Sexta-feira",
Thursday: "Quinta-feira",
Wednesday: "Quarta",
Tuesday: "Terça",
Monday: "Segunda",
Sunday: "Domingo",
Jan:'Jan',
Feb:'Fev',
Mar:'Mar',
Apr:'Abr',
May:'Mai',
Jun:'Jun',
Jul:'Jul',
Aug:'Aug',
Sep:'Sep',
Oct:'Oct',
Nov:'Nov',
Dez:'Dez',
Dezember: "Dezembro",
November: " 'Novembro'",
October: "Outubro",
September: "Setembro",
August: 'Agosto',
July: "Julho",
June: "Junho",
May: "Maio",
April: "Abril",
March: "Março",
February: "Fevereiro",
January: "Janeiro",
LocationLabel: "Mapa",
LocationTextLabel: "Local",
AttendeesLabel: "Participantes",
EndMinLabel: "Min",
EndHourLabel: "Hora",
EndDateLabel: "Data fim",
EndDatePlaceHolder: "Seleccionar data...",
StartMinLabel: "Min",
StartHourLabel: "Hora",
StartDateLabel: "Data Início",
StartDatePlaceHolder: "Seleccionar data...",
EventTitleErrorMessage: "Título do evento é obrigatório.",
EventTitleLabel: "Título do evento",
EventPanelTitle: "Editar/Addcionar Evento",
"PropertyPaneDescription": "Calendário com marcação de eventos, mostra os eventos criados na lista calendário definida no site seleccionado",
"BasicGroupName": "Indique o Url do site e calendário",
"SiteUrlFieldLabel": 'Url do Site',
"ListFieldLabel": 'Nome da Lista Calendario',
"weekLabel": 'Semana',
"dayLable": 'Dia',
"agenda": 'Agenda',
"monthLabel": 'Mês',
"todayLabel": 'Hoje',
"previousLabel": 'Anterior',
"nextLabel": "Seguinte",
"showMore": 'mais'
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,64 @@
/**
* This script updates the package-solution version analogue to the
* the package.json file.
*/
if (process.env.npm_package_version === undefined) {
throw 'Package version cannot be evaluated';
}
// define path to package-solution file
const solution = './config/package-solution.json',
teams = './teams/manifest.json';
// require filesystem instanc
const fs = require('fs');
// get next automated package version from process variable
const nextPkgVersion = process.env.npm_package_version;
// make sure next build version match
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
nextPkgVersion : nextPkgVersion.split('-')[0];
// Update version in SPFx package-solution if exists
if (fs.existsSync(solution)) {
// read package-solution file
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
// parse file as json
const solutionContents = JSON.parse(solutionFileContent);
// set property of version to next version
solutionContents.solution.version = nextVersion + '.0';
// save file
fs.writeFileSync(
solution,
// convert file back to proper json
JSON.stringify(solutionContents, null, 2),
'UTF-8');
}
// Update version in teams manifest if exists
if (fs.existsSync(teams)) {
// read package-solution file
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
// parse file as json
const teamsContent = JSON.parse(teamsManifestContent);
// set property of version to next version
teamsContent.version = nextVersion;
// save file
fs.writeFileSync(
teams,
// convert file back to proper json
JSON.stringify(teamsContent, null, 2),
'UTF-8');
}

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.2/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -85,7 +85,6 @@ Only users with Tenant Admin Role are allowed to managed tenant properties.
![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/dev/samples/react-manage-sitedesigns/assets/screen17.jpg) ![tenant properties](https://github.com/joaojmendes/sp-dev-fx-webparts/blob/dev/samples/react-manage-sitedesigns/assets/screen17.jpg)
@ -127,6 +126,7 @@ Version|Date|Comments
## Minimal Path to Awesome ## Minimal Path to Awesome
- Clone this repository - Clone this repository
- Move to sample folder
- in the command line run: - in the command line run:
- `npm install` - `npm install`
- `gulp build` - `gulp build`
@ -134,7 +134,4 @@ Version|Date|Comments
- `gulp package-solution --ship` - `gulp package-solution --ship`
- `Add to AppCatalog and deploy` - `Add to AppCatalog and deploy`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-manage-sitedesigns" /> <img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-manage-sitedesigns" />