Merge branch 'dev'

This commit is contained in:
VesaJuvonen 2019-06-14 16:30:50 +03:00
commit bfadbf23e8
166 changed files with 90191 additions and 1335 deletions

View File

@ -11,7 +11,7 @@ dist
lib lib
solution solution
temp temp
*.sppkg #*.sppkg
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage

View File

@ -22,7 +22,7 @@
"framework": "react", "framework": "react",
"plusBeta": true, "plusBeta": true,
"isCreatingSolution": true, "isCreatingSolution": true,
"version": "1.8.0", "version": "1.8.2",
"libraryName": "react-calendar", "libraryName": "react-calendar",
"libraryId": "3a13208b-3874-4036-9262-4edd22e88187", "libraryId": "3a13208b-3874-4036-9262-4edd22e88187",
"packageManager": "npm", "packageManager": "npm",

View File

@ -5,13 +5,10 @@ This Web Part allows you to manage events in a calendar.
Uses a list of existing calendars on any website. 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 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. 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 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.
@ -19,11 +16,28 @@ The Web Part does not show recurring events, I will work on it soon.
![callendar](/samples/react-calendar/assets/animatevideo.gif) ![callendar](/samples/react-calendar/assets/animatevideo.gif)
##
![callendar](/samples/react-calendar/assets/weekly_moderncalendar.gif)
##
![callendar](/samples/react-calendar/assets/modercalendar_monthly.gif)
##
![callendar](/samples/react-calendar/assets/moderncalendar_yearly.gif)
## Web Part - Screenshots ## Web Part - Screenshots
![callendar](/samples/react-calendar//assets/screen1.png)
![callendar](/samples/react-calendar/assets/calendar_teams.jpg)
![callendar](/samples/react-calendar/assets/calendar_teams2.jpg)
![callendar](/samples/react-calendar/assets/screen1.png)
![callendar](/samples/react-calendar/assets/screen1.0.jpg) ![callendar](/samples/react-calendar/assets/screen1.0.png)
![callendar](/samples/react-calendar/assets/screen1.1.png) ![callendar](/samples/react-calendar/assets/screen1.1.png)
@ -35,10 +49,10 @@ The Web Part does not show recurring events, I will work on it soon.
![callendar](/samples/react-calendar/assets/screen1.3.png) ![callendar](/samples/react-calendar/assets/screen1.3.png)
![callendar](/samples/react-calendar//assets/screen1.4.png) ![callendar](/samples/react-calendar/assets/screen1.4.png)
![callendar](/samples/react-calendar//assets/screen2.png) ![callendar](/samples/react-calendar/assets/screen2.png)
@ -46,47 +60,45 @@ The Web Part does not show recurring events, I will work on it soon.
![callendar](/samples/react-calendar//assets/screen4.png) ![callendar](/samples/react-calendar/assets/screen4.png)
![callendar](/samples/react-calendar/assets/screen5.png) ![callendar](/samples/react-calendar/assets/screen5.png)
![callendar](/samples/react-calendar//assets/screen6.png) ![callendar](/samples/react-calendar/assets/screen6.png)
![callendar](/samples/react-calendar//assets/screen7.png) ![callendar](/samples/react-calendar/assets/screen7.png)
![callendar](/samples/react-calendar/assets/screen8.png) ![callendar](/samples/react-calendar/assets/screen8.png)
![callendar](/samples/react-calendar//assets/screen9.png) ![callendar](/samples/react-calendar/assets/screen9.png)
## ##
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg) ![drop](https://img.shields.io/badge/version-1.8.2-green.svg)
## Applies to ## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint) * [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) * [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
> Update accordingly as needed.
## WebPart Properties ## WebPart Properties
Property |Type|Required| comments Property |Type|Required| comments
--------------------|----|--------|---------- --------------------|----|--------|----------
Site Url of Calendar List | Text| yes| Site Url of Calendar List | Text| yes|
Calendar list| Text| yes| this is filled with all list of type "event list" created Calendar list| Choice/Dropdown | yes| this is filled with all list of type "event list" created
Start Date | Date | yes | Event Date Start Date | Date | yes | Event Date
End Date| Date| yes | Event Date End Date| Date| yes | Event Date
@ -103,6 +115,7 @@ Calendar Web Part|João Mendes
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0.0|April 25, 2019|Initial release 1.0.0|April 25, 2019|Initial release
1.0.1|June 10, 2019|update add recurrence events
## Disclaimer ## 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.** **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.**
@ -122,4 +135,4 @@ Version|Date|Comments
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" /> <img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-calendar" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

View File

@ -8,6 +8,7 @@
"entrypoint": "./lib/webparts/calendar/CalendarWebPart.js", "entrypoint": "./lib/webparts/calendar/CalendarWebPart.js",
"manifest": "./src/webparts/calendar/CalendarWebPart.manifest.json" "manifest": "./src/webparts/calendar/CalendarWebPart.manifest.json"
} }
] ]
} }
}, },

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "react-calendar", "name": "react-calendar",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -14,12 +14,11 @@
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll" "test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
}, },
"dependencies": { "dependencies": {
"@microsoft/rush-stack-compiler-3.2": "^0.3.6", "@microsoft/sp-core-library": "1.8.2",
"@microsoft/sp-core-library": "1.8.0-plusbeta", "@microsoft/sp-lodash-subset": "1.8.2",
"@microsoft/sp-lodash-subset": "1.8.0-plusbeta", "@microsoft/sp-office-ui-fabric-core": "1.8.2",
"@microsoft/sp-office-ui-fabric-core": "1.8.0-plusbeta", "@microsoft/sp-property-pane": "1.8.2",
"@microsoft/sp-property-pane": "1.8.0-plusbeta", "@microsoft/sp-webpart-base": "1.8.2",
"@microsoft/sp-webpart-base": "1.8.0-plusbeta",
"@pnp/pnpjs": "^1.3.0", "@pnp/pnpjs": "^1.3.0",
"@pnp/spfx-controls-react": "1.12.0", "@pnp/spfx-controls-react": "1.12.0",
"@pnp/spfx-property-controls": "1.14.1", "@pnp/spfx-property-controls": "1.14.1",
@ -29,32 +28,37 @@
"@types/jquery": "^3.3.29", "@types/jquery": "^3.3.29",
"@types/react": "16.7.22", "@types/react": "16.7.22",
"@types/react-big-calendar": "^0.20.13", "@types/react-big-calendar": "^0.20.13",
"@types/react-dom": "16.0.5", "@types/react-dom": "16.8.0",
"@types/webpack-env": "1.13.1", "@types/webpack-env": "1.13.1",
"@uifabric/fluent-theme": "^0.16.7",
"draft-js": "^0.10.5", "draft-js": "^0.10.5",
"draftjs-to-html": "^0.8.4", "draftjs-to-html": "^0.8.4",
"globalize": "^1.4.2", "globalize": "^1.4.2",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"moment": "^2.24.0", "moment": "^2.24.0",
"moment-timezone": "^0.5.25", "office-ui-fabric-react": "6.143.0",
"react": "16.7.0", "react": "16.7.0",
"react-big-calendar": "^0.20.4", "react-big-calendar": "^0.20.4",
"react-dom": "16.7.0", "react-dom": "16.7.0",
"react-draft-wysiwyg": "^1.13.2", "react-draft-wysiwyg": "^1.13.2",
"typescript": "^3.2.4" "spfx-uifabric-themes": "^0.6.0",
"typescript": "^3.2.4",
"xml2js": "^0.4.19"
}, },
"resolutions": { "resolutions": {
"@types/react": "16.4.2" "@types/react": "16.7.22"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/rush-stack-compiler-2.7": "0.4.0", "@microsoft/rush-stack-compiler-2.9": "0.7.7",
"@microsoft/sp-build-web": "1.8.0-plusbeta", "@microsoft/rush-stack-compiler-3.2": "0.3.17",
"@microsoft/sp-module-interfaces": "1.8.0-plusbeta", "@microsoft/sp-build-web": "1.8.2",
"@microsoft/sp-tslint-rules": "1.8.0-plusbeta", "@microsoft/sp-module-interfaces": "1.8.2",
"@microsoft/sp-webpart-workbench": "1.8.0-plusbeta", "@microsoft/sp-tslint-rules": "1.8.2",
"@microsoft/sp-webpart-workbench": "1.8.2",
"@types/chai": "3.4.34", "@types/chai": "3.4.34",
"@types/mocha": "2.2.38", "@types/mocha": "2.2.38",
"@types/xml2js": "^0.4.4",
"@voitanos/jest-preset-spfx-react16": "^1.1.0", "@voitanos/jest-preset-spfx-react16": "^1.1.0",
"ajv": "~5.2.2", "ajv": "~5.2.2",
"gulp": "~3.9.1", "gulp": "~3.9.1",

View File

@ -15,7 +15,6 @@
.description:hover { .description:hover {
border-color: rgb( 51, 51, 51 ); border-color: rgb( 51, 51, 51 );
} }
.calendar { .calendar {
.container { .container {
max-width: 100%; max-width: 100%;
@ -23,13 +22,11 @@
height: 600px; height: 600px;
margin: 0px auto; margin: 0px auto;
} }
.eventTitle { .eventTitle {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.row { .row {
@include ms-Grid-row; @include ms-Grid-row;
@include ms-fontColor-white; @include ms-fontColor-white;
@ -37,7 +34,6 @@
padding: 20px; padding: 20px;
border-style: solid; border-style: solid;
} }
.column { .column {
@include ms-Grid-col; @include ms-Grid-col;
@include ms-lg10; @include ms-lg10;
@ -45,22 +41,18 @@
@include ms-xlPush2; @include ms-xlPush2;
@include ms-lgPush1; @include ms-lgPush1;
} }
.title { .title {
@include ms-font-xl; @include ms-font-xl;
@include ms-fontColor-white; @include ms-fontColor-white;
} }
.subTitle { .subTitle {
@include ms-font-l; @include ms-font-l;
@include ms-fontColor-white; @include ms-fontColor-white;
} }
.description { .description {
@include ms-font-l; @include ms-font-l;
@include ms-fontColor-white; @include ms-fontColor-white;
} }
.button { .button {
// Our button // Our button
text-decoration: none; text-decoration: none;
@ -84,6 +76,7 @@
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
padding: 0 16px; padding: 0 16px;
}
.label { .label {
font-weight: $ms-font-weight-semibold; font-weight: $ms-font-weight-semibold;
@ -95,4 +88,3 @@
display: inline-block; display: inline-block;
} }
} }
}

View File

@ -25,4 +25,8 @@ export interface IEventState {
userPermissions?: IUserPermissions; userPermissions?: IUserPermissions;
isloading:boolean; isloading:boolean;
siteRegionalSettings: any; siteRegionalSettings: any;
recurrenceSeriesEdited?:boolean;
showRecurrenceSeriesInfo:boolean;
newRecurrenceEvent:boolean;
recurrenceAction:string;
} }

View File

@ -37,12 +37,14 @@ import {
Dialog, Dialog,
DialogType, DialogType,
DialogFooter, DialogFooter,
Toggle Toggle,
ActionButton,
IButtonProps
} }
from 'office-ui-fabric-react'; from 'office-ui-fabric-react';
import { addMonths, addYears } from 'office-ui-fabric-react/lib/utilities/dateMath/DateMath'; import { addMonths, addYears } from 'office-ui-fabric-react/lib/utilities/dateMath/DateMath';
import { _ComponentBaseKillSwitches } from '@microsoft/sp-component-base';
import { IPanelModelEnum } from './IPanelModeEnum'; import { IPanelModelEnum } from './IPanelModeEnum';
import { EditorState, convertToRaw, ContentState } from 'draft-js'; import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg'; import { Editor } from 'react-draft-wysiwyg';
@ -51,11 +53,13 @@ import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'; import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import spservices from '../../services/spservices'; import spservices from '../../services/spservices';
import { Map, ICoordinates, MapType } from "@pnp/spfx-controls-react/lib/Map"; import { Map, ICoordinates, MapType } from "@pnp/spfx-controls-react/lib/Map";
import { EventRecurrenceInfo } from '../../controls/EventRecurrenceInfo/EventRecurrenceInfo';
import { string } from 'prop-types';
import { getGUID } from '@pnp/common';
const today: Date = new Date(Date.now()); const today: Date = new Date(Date.now());
const DayPickerStrings: IDatePickerStrings = { 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], months: [strings.January, strings.February, strings.March, strings.April, strings.May, strings.June, strings.July, strings.August, strings.September, strings.October, strings.November, strings.December],
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], 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], 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], shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Tursday, strings.ShortDay_Friday, strings.ShortDay_Saunday],
@ -74,6 +78,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
private attendees: IPersonaProps[] = []; private attendees: IPersonaProps[] = [];
private latitude: number = 41.1931819; private latitude: number = 41.1931819;
private longitude: number = -8.4897452; private longitude: number = -8.4897452;
private returnedRecurrenceInfo: { recurrenceData: string, eventDate: Date, endDate: Date } = undefined;
private categoryDropdownOption: IDropdownOption[] = []; private categoryDropdownOption: IDropdownOption[] = [];
@ -93,7 +98,6 @@ export class Event extends React.Component<IEventProps, IEventState> {
} }
// Initialize Map coordinates // Initialize Map coordinates
console.log('ini', this.latitude, this.longitude);
this.state = { this.state = {
showPanel: false, showPanel: false,
eventData: this.props.event, eventData: this.props.event,
@ -112,6 +116,10 @@ export class Event extends React.Component<IEventProps, IEventState> {
displayDialog: false, displayDialog: false,
isloading: false, isloading: false,
siteRegionalSettings: undefined, siteRegionalSettings: undefined,
recurrenceSeriesEdited: false,
showRecurrenceSeriesInfo:false,
newRecurrenceEvent:false,
recurrenceAction: 'display',
userPermissions: { hasPermissionAdd: false, hasPermissionDelete: false, hasPermissionEdit: false, hasPermissionView: false }, userPermissions: { hasPermissionAdd: false, hasPermissionDelete: false, hasPermissionEdit: false, hasPermissionView: false },
}; };
// local copia of props // local copia of props
@ -131,8 +139,9 @@ export class Event extends React.Component<IEventProps, IEventState> {
this.onDelete = this.onDelete.bind(this); this.onDelete = this.onDelete.bind(this);
this.closeDialog = this.closeDialog.bind(this); this.closeDialog = this.closeDialog.bind(this);
this.confirmDelete = this.confirmDelete.bind(this); this.confirmDelete = this.confirmDelete.bind(this);
this.onAllDayEventChange = this.onAllDayEventChange.bind(this);
this.onCategoryChanged = this.onCategoryChanged.bind(this); this.onCategoryChanged = this.onCategoryChanged.bind(this);
this.onEditRecurrence = this.onEditRecurrence.bind(this);
this.returnRecurrenceInfo = this.returnRecurrenceInfo.bind(this);
this.spService = new spservices(this.props.context); this.spService = new spservices(this.props.context);
} }
/** /**
@ -152,27 +161,58 @@ export class Event extends React.Component<IEventProps, IEventState> {
private async onSave() { private async onSave() {
let eventData: IEventData = this.state.eventData; let eventData: IEventData = this.state.eventData;
// All Day event ? let panelMode = this.props.panelMode;
let startDate: string = null;
let endDate: string = null;
eventData.fRecurrence = false;
// if there are new Event recurrence or Edited recurrence series
if (this.state.recurrenceSeriesEdited || this.state.newRecurrenceEvent) {
eventData.RecurrenceData = this.returnedRecurrenceInfo.recurrenceData;
startDate = `${moment(this.returnedRecurrenceInfo.eventDate).format('YYYY/MM/DD')}`;
endDate = `${moment(this.returnedRecurrenceInfo.endDate).format('YYYY/MM/DD')}`;
if (eventData.EventType == "0" && this.state.newRecurrenceEvent) {
eventData.EventType = "1";
eventData.fRecurrence= true;
eventData.UID = getGUID();
}
if (eventData.EventType == "1" && this.state.recurrenceSeriesEdited) {
eventData.fRecurrence= true;
eventData.UID = getGUID();
}
} else {
if (this.state.eventData.EventType == '1'){ // recurrence exception
eventData.RecurrenceID = eventData.EventDate.toString();
eventData.MasterSeriesItemID = eventData.ID.toString();
eventData.EventType = "4";
eventData.fRecurrence = true;
eventData.UID = getGUID();
panelMode = IPanelModelEnum.add;
}
startDate = `${moment(this.state.startDate).format('YYYY/MM/DD')}`;
endDate = `${moment(this.state.endDate).format('YYYY/MM/DD')}`;
}
const startDate = `${moment(this.state.startDate).format('YYYY/MM/DD')}`;
const startTime = `${this.state.startSelectedHour.key}:${this.state.startSelectedMin.key}`; const startTime = `${this.state.startSelectedHour.key}:${this.state.startSelectedMin.key}`;
const startDateTime = `${startDate} ${startTime}`; const startDateTime = `${startDate} ${startTime}`;
const start = moment(startDateTime, 'YYYY/MM/DD HH:mm').toLocaleString(); const start = moment(startDateTime, 'YYYY/MM/DD HH:mm').toLocaleString();
eventData.start = new Date(start); eventData.EventDate = new Date(start);
// End Date // End Date
const endDate = `${moment(this.state.endDate).format('YYYY/MM/DD')}`;
const endTime = `${this.state.endSelectedHour.key}:${this.state.endSelectedMin.key}`; const endTime = `${this.state.endSelectedHour.key}:${this.state.endSelectedMin.key}`;
const endDateTime = `${endDate} ${endTime}`; const endDateTime = `${endDate} ${endTime}`;
const end = moment(endDateTime, 'YYYY/MM/DD HH:mm').toLocaleString(); const end = moment(endDateTime, 'YYYY/MM/DD HH:mm').toLocaleString();
eventData.end = new Date(end); eventData.EndDate = new Date(end);
// get Geolocation // get Geolocation
eventData.geolocation = { Latitude: this.latitude, Longitude: this.longitude }; eventData.geolocation = { Latitude: this.latitude, Longitude: this.longitude };
const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude); const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude);
eventData.location = locationInfo ? locationInfo.display_name : 'N/A'; eventData.location = locationInfo ? locationInfo.display_name : 'N/A';
console.log('beforeupd',eventData.geolocation);
// get Attendees // get Attendees
if (!eventData.attendes) { //vinitialize if no attendees if (!eventData.attendes) { //vinitialize if no attendees
eventData.attendes = []; eventData.attendes = [];
@ -185,12 +225,12 @@ export class Event extends React.Component<IEventProps, IEventState> {
for (const user of this.attendees) { for (const user of this.attendees) {
const userInfo: any = await this.spService.getUserByLoginName(user.id, this.props.siteUrl); const userInfo: any = await this.spService.getUserByLoginName(user.id, this.props.siteUrl);
eventData.attendes.push(parseInt(userInfo.Id)); eventData.attendes.push(Number(userInfo.Id));
} }
this.setState({ isSaving: true }); this.setState({ isSaving: true });
switch (this.props.panelMode) { switch (panelMode) {
case IPanelModelEnum.edit: case IPanelModelEnum.edit:
await this.spService.updateEvent(eventData, this.props.siteUrl, this.props.listId); await this.spService.updateEvent(eventData, this.props.siteUrl, this.props.listId);
break; break;
@ -215,15 +255,21 @@ export class Event extends React.Component<IEventProps, IEventState> {
* @memberof Event * @memberof Event
*/ */
public componentDidCatch(error: any, errorInfo: any) { public componentDidCatch(error: any, errorInfo: any) {
this.setState({ hasError: true, errorMessage: errorInfo.componentStack }); this.setState({ hasError: true, errorMessage: errorInfo.message });
} }
/** /**
* *
* *
* @private
* @param {number} [eventId]
* @memberof Event * @memberof Event
*/ */
public async componentDidMount() { private async renderEventData(eventId?: number) {
this.setState({ isloading: true }); this.setState({ isloading: true });
const event: IEventData = !eventId ? this.props.event : await this.spService.getEvent(this.props.siteUrl, this.props.listId, eventId);
let editorState: EditorState; let editorState: EditorState;
// Load Regional Settings // Load Regional Settings
const siteRegionalSettigns = await this.spService.getSiteRegionalSettingsTimeZone(this.props.siteUrl); const siteRegionalSettigns = await this.spService.getSiteRegionalSettingsTimeZone(this.props.siteUrl);
@ -232,16 +278,16 @@ export class Event extends React.Component<IEventProps, IEventState> {
// Load Categories // Load Categories
this.categoryDropdownOption = await this.spService.getChoiceFieldOptions(this.props.siteUrl, this.props.listId, 'Category'); this.categoryDropdownOption = await this.spService.getChoiceFieldOptions(this.props.siteUrl, this.props.listId, 'Category');
// Edit Mode ? // Edit Mode ?
if (this.props.panelMode == IPanelModelEnum.edit && this.props.event) { if (this.props.panelMode == IPanelModelEnum.edit && event) {
// Get hours of event // Get hours of event
const startHour = moment(this.props.event.start).format('HH').toString(); const startHour = moment(event.EventDate).format('HH').toString();
const startMin = moment(this.props.event.start).format('mm').toString(); const startMin = moment(event.EventDate).format('mm').toString();
const endHour = moment(this.props.event.end).format('HH').toString(); const endHour = moment(event.EndDate).format('HH').toString();
const endMin = moment(this.props.event.end).format('mm').toString(); const endMin = moment(event.EndDate).format('mm').toString();
// Get Descript and covert to RichText Control // Get Descript and covert to RichText Control
const html = this.props.event.Description; const html = event.Description;
const contentBlock = htmlToDraft(html); const contentBlock = htmlToDraft(html);
if (contentBlock) { if (contentBlock) {
@ -250,7 +296,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
} }
// testa attendees // testa attendees
const attendees = this.props.event.attendes; const attendees = event.attendes;
let selectedUsers: string[] = []; let selectedUsers: string[] = [];
if (attendees && attendees.length > 0) { if (attendees && attendees.length > 0) {
for (const userId of attendees) { for (const userId of attendees) {
@ -261,14 +307,16 @@ export class Event extends React.Component<IEventProps, IEventState> {
} }
} }
// Has geolocation ? // Has geolocation ?
this.latitude = this.props.event.geolocation && this.props.event.geolocation.Latitude ? this.props.event.geolocation.Latitude : this.latitude; this.latitude = event.geolocation && event.geolocation.Latitude ? event.geolocation.Latitude : this.latitude;
this.longitude = this.props.event.geolocation && this.props.event.geolocation.Longitude ? this.props.event.geolocation.Longitude : this.longitude; this.longitude = event.geolocation && event.geolocation.Longitude ? event.geolocation.Longitude : this.longitude;
event.geolocation.Latitude = this.latitude;
event.geolocation.Longitude = this.longitude;
// Update Component Data // Update Component Data
this.setState({ this.setState({
eventData: this.props.event, eventData: event,
startDate: this.props.event.start, startDate: event.EventDate,
endDate: this.props.event.end, endDate: event.EndDate,
startSelectedHour: { key: startHour, text: startHour }, startSelectedHour: { key: startHour, text: startHour },
startSelectedMin: { key: startMin, text: startMin }, startSelectedMin: { key: startMin, text: startMin },
endSelectedHour: { key: endHour, text: endHour }, endSelectedHour: { key: endHour, text: endHour },
@ -290,24 +338,30 @@ export class Event extends React.Component<IEventProps, IEventState> {
userPermissions: userListPermissions, userPermissions: userListPermissions,
isloading: false, isloading: false,
siteRegionalSettings: siteRegionalSettigns, siteRegionalSettings: siteRegionalSettigns,
eventData: { ...event, EventType: "0" },
}); });
} }
} }
/** /**
*
* *
* @memberof Event * @memberof Event
*/ */
public componentWillMount() { public async componentDidMount() {
await this.renderEventData();
} }
/** /**
* @private * @private
* @memberof Event * @memberof Event
*/ */
private onStartChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => { private onStartChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ startSelectedHour: item }); this.setState({ startSelectedHour: item });
} }
@ -316,7 +370,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
* @memberof Event * @memberof Event
*/ */
private onEndChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => { private onEndChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ endSelectedHour: item }); this.setState({ endSelectedHour: item });
} }
@ -325,7 +379,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
* @memberof Event * @memberof Event
*/ */
private onStartChangeMin = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => { private onStartChangeMin = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ startSelectedMin: item }); this.setState({ startSelectedMin: item });
} }
@ -374,12 +428,11 @@ export class Event extends React.Component<IEventProps, IEventState> {
* @memberof Event * @memberof Event
*/ */
private onEndChangeMin(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void { private onEndChangeMin(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
ev.preventDefault();
this.setState({ endSelectedMin: item }); this.setState({ endSelectedMin: item });
} }
/** /**
*
* *
* @private * @private
* @param {React.FormEvent<HTMLDivElement>} ev * @param {React.FormEvent<HTMLDivElement>} ev
@ -387,7 +440,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
* @memberof Event * @memberof Event
*/ */
private onCategoryChanged(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void { private onCategoryChanged(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
ev.preventDefault();
this.setState({ eventData: { ...this.state.eventData, Category: item.text } }); this.setState({ eventData: { ...this.state.eventData, Category: item.text } });
} }
@ -413,6 +466,13 @@ export class Event extends React.Component<IEventProps, IEventState> {
this.setState({ displayDialog: false }); this.setState({ displayDialog: false });
} }
/**
*
*
* @private
* @param {React.MouseEvent<HTMLDivElement>} ev
* @memberof Event
*/
private async confirmDelete(ev: React.MouseEvent<HTMLDivElement>) { private async confirmDelete(ev: React.MouseEvent<HTMLDivElement>) {
ev.preventDefault(); ev.preventDefault();
try { try {
@ -420,7 +480,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
switch (this.props.panelMode) { switch (this.props.panelMode) {
case IPanelModelEnum.edit: case IPanelModelEnum.edit:
await this.spService.deleteEvent(this.state.eventData, this.props.siteUrl, this.props.listId); await this.spService.deleteEvent(this.state.eventData, this.props.siteUrl, this.props.listId, this.state.recurrenceSeriesEdited);
break; break;
default: default:
break; break;
@ -428,7 +488,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
this.setState({ isDeleting: false }); this.setState({ isDeleting: false });
this.props.onDissmissPanel(true); this.props.onDissmissPanel(true);
} catch (error) { } catch (error) {
this.setState({ hasError: true, errorMessage: error.message, isDeleting: false }); this.setState({ hasError: true, errorMessage: error.message, isDeleting: false, displayDialog:false });
} }
} }
@ -445,14 +505,20 @@ export class Event extends React.Component<IEventProps, IEventState> {
</DefaultButton> </DefaultButton>
{ {
this.props.panelMode == IPanelModelEnum.edit && this.state.userPermissions.hasPermissionDelete && ( this.props.panelMode == IPanelModelEnum.edit && this.state.userPermissions.hasPermissionDelete && (
<DefaultButton onClick={this.onDelete} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}> <DefaultButton
onClick={this.onDelete}
style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}>
{strings.DeleteButtonLabel} {strings.DeleteButtonLabel}
</DefaultButton> </DefaultButton>
) )
} }
{ {
(this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit) && (this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit) &&
<PrimaryButton disabled={this.state.disableButton} onClick={this.onSave} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}> <PrimaryButton
disabled={this.state.disableButton}
onClick={this.onSave}
style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}>
{strings.SaveButtonLabel} {strings.SaveButtonLabel}
</PrimaryButton> </PrimaryButton>
@ -485,10 +551,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
} }
private onAllDayEventChange(ev: React.MouseEvent<HTMLElement>, checked: boolean) {
ev.preventDefault();
this.setState({ eventData: { ...this.state.eventData, allDayEvent: checked } });
}
/** /**
* *
* @private * @private
@ -498,13 +561,45 @@ export class Event extends React.Component<IEventProps, IEventState> {
private async onUpdateCoordinates(coordinates: ICoordinates) { private async onUpdateCoordinates(coordinates: ICoordinates) {
this.latitude = coordinates.latitude; this.latitude = coordinates.latitude;
this.longitude = coordinates.longitude; this.longitude = coordinates.longitude;
console.log('upcoor',this.latitude + ' ' + this.longitude);
const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude); const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude);
this.setState({ eventData: { ...this.state.eventData, location: locationInfo.display_name } }); this.setState({ eventData: { ...this.state.eventData, location: locationInfo.display_name } });
} }
/**
*
*
* @private
* @param {React.MouseEvent<HTMLButtonElement>} ev
* @memberof Event
*/
private async onEditRecurrence(ev: React.MouseEvent<HTMLButtonElement>) {
ev.preventDefault();
// EventType = 4 Recurrence Exception
await this.renderEventData(this.state.eventData.EventType == '4' ? Number(this.state.eventData.MasterSeriesItemID) : this.state.eventData.Id);
this.setState({ showRecurrenceSeriesInfo: true, recurrenceSeriesEdited: true });
}
/**
*
*
* @param {Date} startDate
* @param {string} recurrenceData
* @memberof Event
*/
public async returnRecurrenceInfo(startDate: Date, recurrenceData: string) {
this.returnedRecurrenceInfo = { recurrenceData: recurrenceData, eventDate: startDate, endDate: moment().add(20, 'years').toDate() };
//this.setState({ editRecurrenceSeries:false})
//console.log(this.returnedRecurrenceInfo);
}
/**
*
*
* @returns {React.ReactElement<IEventProps>}
* @memberof Event
*/
public render(): React.ReactElement<IEventProps> { public render(): React.ReactElement<IEventProps> {
console.log(this.state.locationLatitude + '-' + this.state.locationLongitude);
const { editorState } = this.state; const { editorState } = this.state;
return ( return (
<div> <div>
@ -531,7 +626,23 @@ export class Event extends React.Component<IEventProps, IEventState> {
{ {
!this.state.isloading && !this.state.isloading &&
<div> <div>
{
(this.state.eventData && (this.state.eventData.EventType !== "0" && this.state.showRecurrenceSeriesInfo !== true)) ?
<div> <div>
<h2 style={{ display: 'inline-block', verticalAlign: 'top' }}>Recurrence Event</h2>
<DefaultButton
style={{ display: 'inline-block', marginLeft: '330px', verticalAlign: 'top', width: 'auto' }}
iconProps={{ iconName: 'RecurringEvent' }}
allowDisabledFocus={true}
onClick={this.onEditRecurrence}
>
Edit Recurrence Series
</DefaultButton>
</div>
: ''
}
<div style={{ marginTop: 10 }} >
<TextField <TextField
label={strings.EventTitleLabel} label={strings.EventTitleLabel}
value={this.state.eventData ? this.state.eventData.title : ''} value={this.state.eventData ? this.state.eventData.title : ''}
@ -562,6 +673,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
label={strings.StartDateLabel} label={strings.StartDateLabel}
onSelectDate={this.onSelectDateStart} onSelectDate={this.onSelectDateStart}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true} disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
hidden={this.state.showRecurrenceSeriesInfo}
/> />
</div> </div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}> <div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
@ -632,6 +744,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
label={strings.EndDateLabel} label={strings.EndDateLabel}
onSelectDate={this.onSelectDateEnd} onSelectDate={this.onSelectDateEnd}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true} disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
hidden={this.state.showRecurrenceSeriesInfo}
/> />
</div> </div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}> <div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
@ -693,6 +806,41 @@ export class Event extends React.Component<IEventProps, IEventState> {
</div> </div>
<Label>{this.state.siteRegionalSettings ? this.state.siteRegionalSettings.Description : ''}</Label> <Label>{this.state.siteRegionalSettings ? this.state.siteRegionalSettings.Description : ''}</Label>
<br /> <br />
{
this.state.eventData && (this.state.eventData.EventType == "0") ?
<div style={{ display: 'inline-block', verticalAlign: 'top', width: '200px' }}>
<Toggle
defaultChecked={false}
inlineLabel={true}
label="Recurrence ?"
onText="On"
offText="Off"
onChange={(ev, checked: boolean) => {
ev.preventDefault();
this.setState({ showRecurrenceSeriesInfo: checked, newRecurrenceEvent: checked });
}}
/>
</div>
:
''
}
{
this.state.showRecurrenceSeriesInfo && (
<EventRecurrenceInfo
context={this.props.context}
display={true}
recurrenceData={this.state.eventData.RecurrenceData}
startDate={this.state.startDate}
siteUrl={this.props.siteUrl}
returnRecurrenceData={this.returnRecurrenceInfo}
>
</EventRecurrenceInfo>
)
}
< Label > Event Description</Label> < Label > Event Description</Label>
<div className={styles.description}> <div className={styles.description}>
@ -760,7 +908,9 @@ export class Event extends React.Component<IEventProps, IEventState> {
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>
</div> </div>
} }
</Panel> </Panel>
</div> </div>
); );

View File

@ -0,0 +1,7 @@
.divWrraper {
border-width:1px;
border-color:#adadad;
padding: 20px;
border-style: solid;
}

View File

@ -0,0 +1,172 @@
import * as React from 'react';
import styles from './EventRecurrenceInfo.module.scss';
import * as strings from 'CalendarWebPartStrings';
import { IEventRecurrenceInfoProps } from './IEventRecurrenceInfoProps';
import { IEventRecurrenceInfoState } from './IEventRecurrenceInfoState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as moment from 'moment';
import { parseString, Builder } from "xml2js";
import {
ChoiceGroup,
IChoiceGroupOption,
} from 'office-ui-fabric-react';
import { EventRecurrenceInfoDaily } from './../EventRecurrenceInfoDaily/EventRecurrenceInfoDaily';
import { EventRecurrenceInfoWeekly } from './../EventRecurrenceInfoWeekly/EventRecurrenceInfoWeekly';
import { EventRecurrenceInfoMonthly } from './../EventRecurrenceInfoMonthly/EventRecurrenceInfoMonthly';
import { EventRecurrenceInfoYearly } from './../EventRecurrenceInfoYearly/EventRecurrenceInfoYearly';
export class EventRecurrenceInfo extends React.Component<IEventRecurrenceInfoProps, IEventRecurrenceInfoState> {
public constructor(props) {
super(props);
this._onRecurrenceFrequenceChange = this._onRecurrenceFrequenceChange.bind(this);
this.state = {
selectedKey: 'daily',
selectPatern: 'every',
startDate: moment().toDate(),
endDate: moment().endOf('month').toDate(),
numberOcurrences: '1',
numberOfDays: '1',
disableNumberOfDays: false,
disableNumberOcurrences: true,
selectdateRangeOption: 'noDate',
disableEndDate: true,
selectedRecurrenceRule: 'daily',
};
}
private _onRecurrenceFrequenceChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
this.setState({
selectedRecurrenceRule: option.key
});
}
/**
*
*
* @memberof EventRecurrenceInfo
*/
public async componentDidMount() {
if (this.props.recurrenceData) {
if (this.props.recurrenceData.indexOf('<daily') != -1) {
this.setState({ selectedRecurrenceRule: 'daily' });
}
if (this.props.recurrenceData.indexOf('<weekly') != -1) {
this.setState({ selectedRecurrenceRule: 'weekly' });
}
if (this.props.recurrenceData.indexOf('<monthly') != -1) {
this.setState({ selectedRecurrenceRule: 'monthly' });
}
if (this.props.recurrenceData.indexOf('<monthlyByDay') != -1) {
this.setState({ selectedRecurrenceRule: 'monthly' });
}
if (this.props.recurrenceData.indexOf('<yearly') != -1) {
this.setState({ selectedRecurrenceRule: 'yearly' });
}
}
}
/**
*
*
* @returns {React.ReactElement<IEventRecurrenceInfoProps>}
* @memberof EventRecurrenceInfo
*/
public render(): React.ReactElement<IEventRecurrenceInfoProps> {
return (
<div className={styles.divWrraper} >
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
<ChoiceGroup
label="Recurrence Information"
selectedKey={this.state.selectedRecurrenceRule}
options={[
{
key: 'daily',
iconProps: { iconName: 'CalendarDay' },
text: 'Daily'
},
{
key: 'weekly',
iconProps: { iconName: 'CalendarWeek' },
text: 'Weekly'
},
{
key: 'monthly',
iconProps: { iconName: 'Calendar' },
text: 'Monthly',
},
{
key: 'yearly',
iconProps: { iconName: 'Calendar' },
text: 'Yearly',
}
]}
onChange={this._onRecurrenceFrequenceChange}
/>
</div>
{
this.state.selectedRecurrenceRule === 'daily' && (
<EventRecurrenceInfoDaily
display={true}
recurrenceData={this.props.recurrenceData}
startDate={this.props.startDate}
context={this.props.context}
siteUrl={this.props.siteUrl}
returnRecurrenceData={this.props.returnRecurrenceData}
/>
)
}
{
this.state.selectedRecurrenceRule === 'weekly' && (
<EventRecurrenceInfoWeekly
display={true}
recurrenceData={this.props.recurrenceData}
startDate={this.props.startDate}
context={this.props.context}
siteUrl={this.props.siteUrl}
returnRecurrenceData={this.props.returnRecurrenceData}
/>
)
}
{
this.state.selectedRecurrenceRule === 'monthly' && (
<EventRecurrenceInfoMonthly
display={true}
recurrenceData={this.props.recurrenceData}
startDate={this.props.startDate}
context={this.props.context}
siteUrl={this.props.siteUrl}
returnRecurrenceData={this.props.returnRecurrenceData}
/>
)
}
{
this.state.selectedRecurrenceRule === 'yearly' && (
<EventRecurrenceInfoYearly
display={true}
recurrenceData={this.props.recurrenceData}
startDate={this.props.startDate}
context={this.props.context}
siteUrl={this.props.siteUrl}
returnRecurrenceData={this.props.returnRecurrenceData}
/>
)
}
</div>
);
}
}

View File

@ -0,0 +1,9 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEventRecurrenceInfoProps {
display:boolean;
recurrenceData: string;
startDate:Date;
context:WebPartContext;
siteUrl:string;
returnRecurrenceData: (startDate:Date,recurrenceData:string) => void;
}

View File

@ -0,0 +1,13 @@
export interface IEventRecurrenceInfoState {
selectedKey:string;
selectPatern:string;
startDate: Date;
endDate:Date;
numberOcurrences:string;
numberOfDays:string;
disableNumberOfDays: boolean;
disableNumberOcurrences: boolean;
selectdateRangeOption:string;
disableEndDate:boolean;
selectedRecurrenceRule:string;
}

View File

@ -0,0 +1,6 @@
.divWrraper {
border-width:1px;
border-color:#adadad;
padding: 20px;
border-style: solid;
}

View File

@ -0,0 +1,408 @@
import * as React from 'react';
import styles from './EventRecurrenceInfoDaily.module.scss';
import * as strings from 'CalendarWebPartStrings';
import { IEventRecurrenceInfoDailyProps } from './IEventRecurrenceInfoDailyProps';
import { IEventRecurrenceInfoDailyState } from './IEventRecurrenceInfoDailyState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as moment from 'moment';
import { parseString, Builder } from "xml2js";
import {
ChoiceGroup,
IChoiceGroupOption,
Label,
MaskedTextField,
} from 'office-ui-fabric-react';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import spservices from '../../services/spservices';
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.December],
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 EventRecurrenceInfoDaily
* @extends {React.Component<IEventRecurrenceInfoDailyProps, IEventRecurrenceInfoDailyState>}
*/
export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceInfoDailyProps, IEventRecurrenceInfoDailyState> {
private spService: spservices = null;
public constructor(props) {
super(props);
this.onPaternChange = this.onPaternChange.bind(this);
this.state = {
selectedKey: 'daily',
selectPatern: 'every',
startDate: this.props.startDate ? this.props.startDate : moment().toDate(),
endDate: moment().endOf('month').toDate(),
numberOcurrences: '1',
numberOfDays: '1',
disableNumberOfDays: false,
disableNumberOcurrences: true,
selectdateRangeOption: 'noDate',
disableEndDate: true,
selectedRecurrenceRule: 'daily',
isLoading: false,
errorMessageNumberOcurrences: '',
errorMessageNumberOfDays: '',
};
//
this.onNumberOfDaysChange = this.onNumberOfDaysChange.bind(this);
this.onNumberOfOcurrencesChange = this.onNumberOfOcurrencesChange.bind(this);
this.onDataRangeOptionChange = this.onDataRangeOptionChange.bind(this);
this.onEndDateChange = this.onEndDateChange.bind(this);
this.onStartDateChange = this.onStartDateChange.bind(this);
this.onApplyRecurrence = this.onApplyRecurrence.bind(this);
this.spService = new spservices(this.props.context);
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onStartDateChange(date: Date) {
this.setState({ startDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onEndDateChange(date: Date) {
this.setState({ endDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onNumberOfDaysChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
let errorMessage = '';
setTimeout(() => {
if (Number(value.trim()) == 0 || Number(value.trim()) > 255) {
value = '1 ';
errorMessage = 'Allowed values 1 to 255';
}
this.setState({ numberOfDays: value, errorMessageNumberOfDays: errorMessage });
this.applyRecurrence();
}, 2500);
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onNumberOfOcurrencesChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
let errorMessage = '';
setTimeout(() => {
if (Number(value.trim()) == 0 || Number(value.trim()) > 999) {
value = '1 ';
errorMessage = 'Allowed values 1 to 999';
}
this.setState({ numberOcurrences: value , errorMessageNumberOcurrences: errorMessage });
this.applyRecurrence();
}, 2500);
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {IChoiceGroupOption} option
* @memberof EventRecurrenceInfoDaily
*/
private onDataRangeOptionChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectdateRangeOption: option.key,
disableNumberOcurrences: option.key == 'endAfter' ? false : true,
disableEndDate: option.key == 'endDate' ? false : true,
});
this.applyRecurrence();
}
private onPaternChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectPatern: option.key,
disableNumberOfDays: option.key == 'every' ? false : true,
});
this.applyRecurrence();
}
public async componentWillMount() {
// await this.load();
await this.load();
}
public async componentDidUpdate(prevProps: IEventRecurrenceInfoDailyProps, prevState: IEventRecurrenceInfoDailyState) {
}
private async load() {
let patern: any = {};
let dateRange: { repeatForever?: string, repeatInstances?: string, windowEnd?: Date } = {};
let dailyPatern: { dayFrequency?: string, weekDay?: string } = {};
let recurrenceRule: string;
if (this.props.recurrenceData) {
parseString(this.props.recurrenceData, { explicitArray: false }, (error, result) => {
if (result.recurrence.rule.repeat) {
patern = result.recurrence.rule.repeat;
}
//
if (result.recurrence.rule.repeatForever) {
dateRange = { repeatForever: result.recurrence.rule.repeatForever };
}
if (result.recurrence.rule.repeatInstances) {
dateRange = { repeatInstances: result.recurrence.rule.repeatInstances };
}
if (result.recurrence.rule.windowEnd) {
dateRange = { windowEnd: result.recurrence.rule.windowEnd };
}
});
// daily Patern
if (patern.daily) {
recurrenceRule = 'daily';
if (patern.daily.$.dayFrequency) {
dailyPatern = { dayFrequency: patern.daily.$.dayFrequency };
}
if (patern.daily.$.weekday) {
dailyPatern = { weekDay: 'weekDay' };
}
}
let selectDateRangeOption: string = 'noDate';
if (dateRange.repeatForever) {
selectDateRangeOption = 'noDate';
} else if (dateRange.repeatInstances) {
selectDateRangeOption = 'endAfter';
} else if (dateRange.windowEnd) {
selectDateRangeOption = 'endDate';
}
// weekday patern
this.setState({
selectedRecurrenceRule: recurrenceRule,
selectPatern: dailyPatern.dayFrequency ? 'every' : 'everweekday',
numberOfDays: dailyPatern.dayFrequency ? dailyPatern.dayFrequency : '1',
disableNumberOfDays: dailyPatern.dayFrequency ? false : true,
selectdateRangeOption: selectDateRangeOption,
numberOcurrences: dateRange.repeatInstances ? dateRange.repeatInstances : '10',
disableNumberOcurrences: dateRange.repeatInstances ? false : true,
endDate: dateRange.windowEnd ? new Date(moment(dateRange.windowEnd).format('YYYY/MM/DD')) : this.state.endDate,
disableEndDate: dateRange.windowEnd ? false : true,
isLoading: false,
});
}
await this.applyRecurrence();
}
private async onApplyRecurrence(ev: React.MouseEvent<HTMLButtonElement>) {
await this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.MouseEvent<HTMLButtonElement>} ev
* @memberof EventRecurrenceInfoDaily
*/
private async applyRecurrence() {
const siteTimeZoneHours: number = await this.spService.getSiteTimeZoneHours(this.props.siteUrl);
const eventDate = new Date(moment(this.state.startDate).add(siteTimeZoneHours, 'hours').toISOString());
const endDate = moment(this.state.endDate).add(siteTimeZoneHours, 'hours').toISOString();
let selectDateRangeOption;
switch (this.state.selectdateRangeOption) {
case 'noDate':
selectDateRangeOption = `<repeatForever>FALSE</repeatForever>`;
break;
case 'endAfter':
selectDateRangeOption = `<repeatInstances>${this.state.numberOcurrences}</repeatInstances>`;
break;
case 'endDate':
selectDateRangeOption = `<windowEnd>${endDate}</windowEnd>`;
break;
default:
break;
}
const recurrenceXML = `<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat>` +
`<daily ${ this.state.selectPatern === 'every' ? `dayFrequency="${this.state.numberOfDays.trim()}"/>` : 'weekday'}</repeat>${selectDateRangeOption}</rule></recurrence>`;
// console.log(recurrenceXML);
this.props.returnRecurrenceData(this.state.startDate, recurrenceXML);
}
/**
*
*
* @returns {React.ReactElement<IEventRecurrenceInfoDailyProps>}
* @memberof EventRecurrenceInfoDaily
*/
public render(): React.ReactElement<IEventRecurrenceInfoDailyProps> {
return (
<div >
{
<div>
<div style={{ display: 'inline-block', float: 'right', paddingTop: '10px', height: '40px' }}>
</div>
<div style={{ width: '100%', paddingTop: '10px' }}>
<Label>Patern</Label>
<ChoiceGroup
selectedKey={this.state.selectPatern}
options={[
{
key: 'every',
text: strings.every,
ariaLabel: 'every',
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="999"
maskChar=' '
disabled={this.state.disableNumberOfDays}
value={this.state.numberOfDays}
errorMessage={this.state.errorMessageNumberOfDays}
onChange={this.onNumberOfDaysChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '60px', paddingLeft: '10px' } }}>{strings.days}</Label>
</div>
);
}
},
{
key: 'everweekday',
text: strings.everyweekdays,
}
]}
onChange={this.onPaternChange}
required={true}
/>
</div>
<div style={{ paddingTop: '22px' }}>
<Label>Date Range</Label>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: '35px', paddingTop: '10px' }}>
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingTop: '10px' }}>
<ChoiceGroup
selectedKey={this.state.selectdateRangeOption}
onChange={this.onDataRangeOptionChange}
options={[
{
key: 'noDate',
text: strings.noEndDate,
},
{
key: 'endDate',
text: strings.EndByLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel="Select a date"
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>
</div>
);
}
},
{
key: 'endAfter',
text: strings.EndAfterLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="999"
maskChar=' '
value={this.state.numberOcurrences}
disabled={this.state.disableNumberOcurrences}
errorMessage={this.state.errorMessageNumberOcurrences}
onChange={this.onNumberOfOcurrencesChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', paddingLeft: '10px' } }}>Ocurrences</Label>
</div>
);
}
},
]}
required={true}
/>
</div>
</div>
</div>
}
</div>
);
}
}

View File

@ -0,0 +1,9 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEventRecurrenceInfoDailyProps {
display:boolean;
recurrenceData: string;
startDate:Date;
context: WebPartContext;
siteUrl:string;
returnRecurrenceData: (startDate:Date,recurrenceData:string) => void;
}

View File

@ -0,0 +1,16 @@
export interface IEventRecurrenceInfoDailyState {
selectedKey:string;
selectPatern:string;
startDate: Date;
endDate:Date;
numberOcurrences:string;
numberOfDays:string;
disableNumberOfDays: boolean;
disableNumberOcurrences: boolean;
selectdateRangeOption:string;
disableEndDate:boolean;
selectedRecurrenceRule:string;
isLoading:boolean;
errorMessageNumberOfDays: string;
errorMessageNumberOcurrences: string;
}

View File

@ -0,0 +1,6 @@
.divWrraper {
border-width:1px;
border-color:#adadad;
padding: 20px;
border-style: solid;
}

View File

@ -0,0 +1,645 @@
import * as React from 'react';
import styles from './EventRecurrenceInfoMonthly.module.scss';
import * as strings from 'CalendarWebPartStrings';
import { IEventRecurrenceInfoMonthlyProps } from './IEventRecurrenceInfoMonthlyProps';
import { IEventRecurrenceInfoMonthlyState } from './IEventRecurrenceInfoMonthlyState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as moment from 'moment';
import { parseString, Builder } from "xml2js";
import {
ChoiceGroup,
IChoiceGroupOption,
Dropdown,
IDropdownOption,
Label,
MaskedTextField,
} from 'office-ui-fabric-react';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import spservices from '../../services/spservices';
import { string } from 'prop-types';
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.December],
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 EventRecurrenceInfoDaily
* @extends {React.Component<IEventRecurrenceInfoMonthlyProps, IEventRecurrenceInfoMonthlyState>}
*/
export class EventRecurrenceInfoMonthly extends React.Component<IEventRecurrenceInfoMonthlyProps, IEventRecurrenceInfoMonthlyState> {
private spService: spservices = null;
public constructor(props) {
super(props);
this.onPaternChange = this.onPaternChange.bind(this);
this.state = {
selectedKey: 'daily',
selectPatern: 'monthly',
startDate: this.props.startDate ? this.props.startDate : moment().toDate(),
endDate: moment(this.props.startDate).add('month', 1).toDate(),
numberOcurrences: '10',
disableDayOfMonth: false,
disableNumberOcurrences: true,
selectdateRangeOption: 'noDate',
disableEndDate: true,
selectedRecurrenceRule: 'monthly',
dayOfMonth: this.props.startDate ? moment(this.props.startDate).format('D') : moment().format('D'),
everyNumberOfMonths: '1',
isLoading: false,
errorMessageNumberOfMonth: '',
errorMessageDayOfMonth: '',
selectedWeekOrderMonth: 'first',
selectedWeekDay: 'day',
errorMessageNumberOfMonthWeekDay: '',
everyNumberOfMonthsWeekDay: '1',
};
//
this.onDayOfMonthChange = this.onDayOfMonthChange.bind(this);
this.onNumberOfOcurrencesChange = this.onNumberOfOcurrencesChange.bind(this);
this.onDataRangeOptionChange = this.onDataRangeOptionChange.bind(this);
this.onEndDateChange = this.onEndDateChange.bind(this);
this.onStartDateChange = this.onStartDateChange.bind(this);
this.onApplyRecurrence = this.onApplyRecurrence.bind(this);
this.onDayOfMonthGetErrorMessage = this.onDayOfMonthGetErrorMessage.bind(this);
this.onEveryNumberOfMonthsChange = this.onEveryNumberOfMonthsChange.bind(this);
this.onEveryNumberOfMonthsWeekDayChange = this.onEveryNumberOfMonthsWeekDayChange.bind(this);
this.onSelectedWeekDayChange = this.onSelectedWeekDayChange.bind(this);
this.onWeekOrderMonthChange = this.onWeekOrderMonthChange.bind(this);
this.spService = new spservices(this.props.context);
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onStartDateChange(date: Date) {
this.setState({ startDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onEndDateChange(date: Date) {
this.setState({ endDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onDayOfMonthChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
setTimeout(() => {
let errorMessage = '';
if (Number(value.trim()) == 0 || Number(value.trim()) > 31) {
value = '1 ';
errorMessage = 'Allowed values 1 to 31';
}
this.setState({ dayOfMonth: value, errorMessageDayOfMonth: errorMessage });
this.applyRecurrence();
}, 2500);
}
/**
*
*
* @private
* @param {string} value
* @returns
* @memberof EventRecurrenceInfoMonthly
*/
private onDayOfMonthGetErrorMessage(value: string) {
return (Number(value.trim()) != 0 && Number(value.trim()) <= 31) ? '' : "Day must be beteween 1 and 31";
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoMonthly
*/
private onEveryNumberOfMonthsChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
setTimeout(() => {
let errorMessage = '';
if (Number(value.trim()) == 0 || Number(value.trim()) > 12) {
value = '1 ';
errorMessage = 'Allowed values 1 to 12';
}
this.setState({ everyNumberOfMonths: value, errorMessageNumberOfMonth: errorMessage });
this.applyRecurrence();
}, 2500);
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoMonthly
*/
private onEveryNumberOfMonthsWeekDayChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
setTimeout(() => {
let errorMessage = '';
if (Number(value.trim()) == 0 || Number(value.trim()) > 12) {
value = '1 ';
errorMessage = strings.AllowedValues1to12Label;
}
this.setState({ everyNumberOfMonthsWeekDay: value, errorMessageNumberOfMonthWeekDay: errorMessage });
this.applyRecurrence();
}, 2500);
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onNumberOfOcurrencesChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
this.setState({ numberOcurrences: value });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {IChoiceGroupOption} option
* @memberof EventRecurrenceInfoDaily
*/
private onDataRangeOptionChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectdateRangeOption: option.key,
disableNumberOcurrences: option.key == 'endAfter' ? false : true,
disableEndDate: option.key == 'endDate' ? false : true,
});
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {IChoiceGroupOption} option
* @memberof EventRecurrenceInfoMonthly
*/
private onPaternChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectPatern: option.key,
disableDayOfMonth: option.key == 'monthly' ? false : true,
});
this.applyRecurrence();
}
public async componentWillMount() {
await this.load();
}
/**
*
*
* @private
* @param {React.FormEvent<HTMLDivElement>} ev
* @param {IDropdownOption} item
* @memberof EventRecurrenceInfoMonthly
*/
private onWeekOrderMonthChange(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption):void {
this.setState({selectedWeekOrderMonth: item.text});
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.FormEvent<HTMLDivElement>} ev
* @param {IDropdownOption} item
* @memberof EventRecurrenceInfoMonthly
*/
private onSelectedWeekDayChange(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption):void {
this.setState({selectedWeekDay: item.key});
this.applyRecurrence();
}
public async componentDidUpdate(prevProps: IEventRecurrenceInfoMonthlyProps, prevState: IEventRecurrenceInfoMonthlyState) {
}
/**
*
*
* @private
* @memberof EventRecurrenceInfoMonthly
*/
private async load() {
let patern: any = {};
let dateRange: { repeatForever?: string, repeatInstances?: string, windowEnd?: Date } = {};
let monthlyPatern: { monthFrequency?: string, day?: string } = {};
let monthlyByDayPatern: { monthFrequency?: string, weekdayOfMonth?: string, weekDay?: string } = {};
let recurrenceRule: string;
if (this.props.recurrenceData) {
parseString(this.props.recurrenceData, { explicitArray: false }, (error, result) => {
if (result.recurrence.rule.repeat) {
patern = result.recurrence.rule.repeat;
}
//
if (result.recurrence.rule.repeatForever) {
dateRange = { repeatForever: result.recurrence.rule.repeatForever };
}
if (result.recurrence.rule.repeatInstances) {
dateRange = { repeatInstances: result.recurrence.rule.repeatInstances };
}
if (result.recurrence.rule.windowEnd) {
dateRange = { windowEnd: result.recurrence.rule.windowEnd };
}
});
// monthly Patern
if (patern.monthly) {
recurrenceRule = 'monthly';
if (patern.monthly.$.monthFrequency && patern.monthly.$.day) {
monthlyPatern = { monthFrequency: patern.monthly.$.monthFrequency, day: patern.monthly.$.day };
}
}
// monthlyByDay Patern
if (patern.monthlyByDay) {
recurrenceRule = 'monthly';
let weekDay = 'day';
if (patern.monthlyByDay.$.su) weekDay = 'sunday';
if (patern.monthlyByDay.$.mo) weekDay = 'monday';
if (patern.monthlyByDay.$.tu) weekDay = 'tuesday';
if (patern.monthlyByDay.$.we) weekDay = 'wednesday';
if (patern.monthlyByDay.$.th) weekDay = 'thursday';
if (patern.monthlyByDay.$.fr) weekDay = 'friday';
if (patern.monthlyByDay.$.sa) weekDay = 'saturday';
if (patern.monthlyByDay.$.day) weekDay = 'day';
if (patern.monthlyByDay.$.weekday) weekDay = 'weekday';
if (patern.monthlyByDay.$.weekend_day) weekDay = 'weekdendday';
monthlyByDayPatern = {
monthFrequency: patern.monthlyByDay.$.monthFrequency,
weekdayOfMonth: patern.monthlyByDay.$.weekdayOfMonth,
weekDay: weekDay,
};
}
let selectDateRangeOption: string = 'noDate';
if (dateRange.repeatForever) {
selectDateRangeOption = 'noDate';
} else if (dateRange.repeatInstances) {
selectDateRangeOption = 'endAfter';
} else if (dateRange.windowEnd) {
selectDateRangeOption = 'endDate';
}
// weekday patern
this.setState({
selectedRecurrenceRule: recurrenceRule,
selectPatern: patern.monthly ? 'monthly' : 'monthlyByDay',
dayOfMonth: monthlyPatern.day ? monthlyPatern.day : '1',
everyNumberOfMonths: monthlyPatern.monthFrequency ? monthlyPatern.monthFrequency : monthlyByDayPatern.monthFrequency ,
everyNumberOfMonthsWeekDay: monthlyByDayPatern.monthFrequency ? monthlyByDayPatern.monthFrequency : '1',
selectedWeekOrderMonth: monthlyByDayPatern.weekdayOfMonth ? monthlyByDayPatern.weekdayOfMonth : 'first',
selectedWeekDay: monthlyByDayPatern.weekDay ? monthlyByDayPatern.weekDay : 'day',
disableDayOfMonth: patern.monthly ? false : true,
selectdateRangeOption: selectDateRangeOption,
numberOcurrences: dateRange.repeatInstances ? dateRange.repeatInstances : '10',
disableNumberOcurrences: dateRange.repeatInstances ? false : true,
endDate: dateRange.windowEnd ? new Date(moment(dateRange.windowEnd).format('YYYY/MM/DD')) : this.state.endDate,
disableEndDate: dateRange.windowEnd ? false : true,
isLoading: false,
});
}
await this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.MouseEvent<HTMLButtonElement>} ev
* @memberof EventRecurrenceInfoMonthly
*/
private async onApplyRecurrence(ev: React.MouseEvent<HTMLButtonElement>) {
await this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.MouseEvent<HTMLButtonElement>} ev
* @memberof EventRecurrenceInfoDaily
*/
private async applyRecurrence() {
const siteTimeZoneHours: number = await this.spService.getSiteTimeZoneHours(this.props.siteUrl);
const eventDate = new Date(moment(this.state.startDate).add(siteTimeZoneHours, 'hours').toISOString());
const endDate = moment(this.state.endDate).add(siteTimeZoneHours, 'hours').toISOString();
let selectDateRangeOption;
switch (this.state.selectdateRangeOption) {
case 'noDate':
selectDateRangeOption = `<repeatForever>FALSE</repeatForever>`;
break;
case 'endAfter':
selectDateRangeOption = `<repeatInstances>${this.state.numberOcurrences}</repeatInstances>`;
break;
case 'endDate':
selectDateRangeOption = `<windowEnd>${endDate}</windowEnd>`;
break;
default:
break;
}
let recurrencePatern: string = '';
if (this.state.selectPatern == 'monthly') {
recurrencePatern = `<monthly monthFrequency="${this.state.everyNumberOfMonths}" day="${this.state.dayOfMonth}" /></repeat>${selectDateRangeOption}</rule></recurrence>`;
}
if (this.state.selectPatern == 'monthlyByDay') {
recurrencePatern = `<monthlyByDay weekdayOfMonth="${this.state.selectedWeekOrderMonth}" `;
switch (this.state.selectedWeekDay) {
case 'day':
recurrencePatern = recurrencePatern + `day="TRUE"`;
break;
case 'weekday':
recurrencePatern = recurrencePatern + `weekday="TRUE"`;
break;
case 'weekendday':
recurrencePatern = recurrencePatern + `weekend_day="TRUE"`;
break;
case 'sunday':
recurrencePatern = recurrencePatern + `su="TRUE"`;
break;
case 'monday':
recurrencePatern = recurrencePatern + `mo="TRUE"`;
break;
case 'tuesday':
recurrencePatern = recurrencePatern + `tu="TRUE"`;
break;
case 'wednesday':
recurrencePatern = recurrencePatern + `we="TRUE"`;
break;
case 'thursday':
recurrencePatern = recurrencePatern + `th="TRUE"`;
break;
case 'friday':
recurrencePatern = recurrencePatern + `fr="TRUE"`;
break;
case 'saturday':
recurrencePatern = recurrencePatern + `sa="TRUE"`;
break;
default:
break;
}
recurrencePatern = recurrencePatern + ` monthFrequency="${this.state.everyNumberOfMonthsWeekDay}" /></repeat>${selectDateRangeOption}</rule></recurrence>`;
}
const recurrenceXML = `<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat>` + recurrencePatern;
// console.log(recurrenceXML);
this.props.returnRecurrenceData(this.state.startDate, recurrenceXML);
}
/**
*
*
* @returns {React.ReactElement<IEventRecurrenceInfoDailyProps>}
* @memberof EventRecurrenceInfoDaily
*/
public render(): React.ReactElement<IEventRecurrenceInfoMonthlyProps> {
return (
<div >
{
<div>
<div style={{ display: 'inline-block', float: 'right', paddingTop: '10px', height: '40px' }}>
</div>
<div style={{ width: '100%', paddingTop: '10px' }}>
<Label>Patern</Label>
<ChoiceGroup
selectedKey={this.state.selectPatern}
options={[
{
key: 'monthly',
text: strings.dayLable,
ariaLabel: strings.dayLable,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="99"
maskChar=' '
disabled={this.state.disableDayOfMonth}
value={this.state.dayOfMonth}
errorMessage={this.state.errorMessageDayOfMonth}
onChange={this.onDayOfMonthChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '65px', paddingLeft: '10px' } }}>{strings.ofEveryLabel}</Label>
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="99"
maskChar=' '
disabled={this.state.disableDayOfMonth}
value={this.state.everyNumberOfMonths}
errorMessage={this.state.errorMessageNumberOfMonth}
onChange={this.onEveryNumberOfMonthsChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}>{strings.MonthsLabel}</Label>
</div>
);
}
},
{
key: 'monthlyByDay',
text: strings.theLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<div style={{ display: 'inline-block', verticalAlign: 'top', width: '90px', paddingLeft: '10px' }}>
<Dropdown
selectedKey={this.state.selectedWeekOrderMonth}
onChange={this.onWeekOrderMonthChange}
disabled={!this.state.disableDayOfMonth}
options={[
{ key: 'first', text: strings.firstLabel },
{ key: 'second', text:strings.secondLabel},
{ key: 'third', text: strings.thirdLabel },
{ key: 'fourth', text:strings.fourthLabel },
{ key: 'last', text: strings.lastLabel },
]}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '5px' }}>
<Dropdown
selectedKey={this.state.selectedWeekDay}
disabled={!this.state.disableDayOfMonth}
onChange={this.onSelectedWeekDayChange}
options={[
{ key: 'day', text: strings.dayLable },
{ key: 'weekday', text: strings.weekDayLabel },
{ key: 'weekendday', text:strings.weekEndDay },
{ key: 'sunday', text: strings.Sunday},
{ key: 'monday', text: strings.Monday },
{ key: 'tuesday', text: strings.Tuesday },
{ key: 'wednesday', text: strings.Wednesday },
{ key: 'thursday', text: strings.Thursday},
{ key: 'friday', text: strings.Friday },
{ key: 'saturday', text: strings.Saturday },
]}
/>
</div>
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '65px', paddingLeft: '10px' } }}>{strings.ofEveryLabel}</Label>
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="99"
maskChar=' '
disabled={!this.state.disableDayOfMonth}
value={this.state.everyNumberOfMonthsWeekDay}
errorMessage={this.state.errorMessageNumberOfMonthWeekDay}
onChange={this.onEveryNumberOfMonthsWeekDayChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '80px', paddingLeft: '10px' } }}>{strings.MonthsLabel}</Label>
</div>
);
}
}
]}
onChange={this.onPaternChange}
required={true}
/>
</div>
<div style={{ paddingTop: '22px' }}>
<Label>{strings.dateRangeLabel}</Label>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: '35px', paddingTop: '10px' }}>
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingTop: '10px' }}>
<ChoiceGroup
selectedKey={this.state.selectdateRangeOption}
onChange={this.onDataRangeOptionChange}
options={[
{
key: 'noDate',
text: strings.noEndDate,
},
{
key: 'endDate',
text: strings.EndByLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>
</div>
);
}
},
{
key: 'endAfter',
text: strings.EndAfterLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="999"
maskChar=' '
value={this.state.numberOcurrences}
disabled={this.state.disableNumberOcurrences}
onChange={this.onNumberOfOcurrencesChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', paddingLeft: '10px' } }}>{strings.OcurrencesLabel}</Label>
</div>
);
}
},
]}
required={true}
/>
</div>
</div>
</div>
}
</div>
);
}
}

View File

@ -0,0 +1,9 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEventRecurrenceInfoMonthlyProps {
display:boolean;
recurrenceData: string;
startDate:Date;
context: WebPartContext;
siteUrl:string;
returnRecurrenceData: (startDate:Date,recurrenceData:string) => void;
}

View File

@ -0,0 +1,21 @@
export interface IEventRecurrenceInfoMonthlyState {
selectedKey:string;
selectPatern:string;
startDate: Date;
endDate:Date;
numberOcurrences:string;
dayOfMonth:string;
everyNumberOfMonths: string;
disableDayOfMonth: boolean;
disableNumberOcurrences: boolean;
selectdateRangeOption:string;
disableEndDate:boolean;
selectedRecurrenceRule:string;
isLoading:boolean;
errorMessageDayOfMonth:string;
errorMessageNumberOfMonth:string;
selectedWeekOrderMonth:string;
selectedWeekDay:string | number;
everyNumberOfMonthsWeekDay:string;
errorMessageNumberOfMonthWeekDay:string;
}

View File

@ -0,0 +1,18 @@
.divWrraper {
border-width:1px;
border-color:#adadad;
padding: 20px;
border-style: solid;
}
.ckeckBoxInline {
display: inline-block;
vertical-align: top;
width: 80px;
padding-left: 10px;
}
.dateRange{
display: inline-block;
vertical-align: top;
padding-right: 35px;
padding-top: 10px;
}

View File

@ -0,0 +1,481 @@
import * as React from 'react';
import styles from './EventRecurrenceInfoWeekly.module.scss';
import * as strings from 'CalendarWebPartStrings';
import { IEventRecurrenceInfoWeeklyProps } from './IEventRecurrenceInfoWeeklyProps';
import { IEventRecurrenceInfoWeeklyState } from './IEventRecurrenceInfoWeeklyState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as moment from 'moment';
import { parseString, Builder } from "xml2js";
import {
ChoiceGroup,
IChoiceGroupOption,
Dropdown,
IDropdownOption,
TextField,
SpinButton,
Label,
PrimaryButton,
MaskedTextField,
CommandBarButton, IButtonProps,
DefaultButton,
Checkbox,
} from 'office-ui-fabric-react';
import { Position } from 'office-ui-fabric-react/lib/utilities/positioning';
import { Root } from '@pnp/graph';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import spservices from '../../services/spservices';
const DayPickerStrings: IDatePickerStrings = {
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
goToToday: 'Go to today',
prevMonthAriaLabel: 'Go to previous month',
nextMonthAriaLabel: 'Go to next month',
prevYearAriaLabel: 'Go to previous year',
nextYearAriaLabel: 'Go to next year',
closeButtonAriaLabel: 'Close date picker'
};
/**
*
*
* @export
* @class EventRecurrenceInfoDaily
* @extends {React.Component<IEventRecurrenceInfoWeeklyProps, IEventRecurrenceInfoWeeklyState>}
*/
export class EventRecurrenceInfoWeekly extends React.Component<IEventRecurrenceInfoWeeklyProps, IEventRecurrenceInfoWeeklyState> {
private spService: spservices = null;
public constructor(props) {
super(props);
this.onPaternChange = this.onPaternChange.bind(this);
this.state = {
selectedKey: 'daily',
selectPatern: 'every',
startDate: this.props.startDate ? this.props.startDate : moment().toDate(),
endDate: moment().endOf('month').toDate(),
numberOcurrences: '10',
numberOfWeeks: '1',
disableNumberOfWeeks: false,
disableNumberOcurrences: true,
selectdateRangeOption: 'noDate',
disableEndDate: true,
weeklySunday: moment().weekday() === 0 ? true: false,
weeklyMonday: moment().weekday() === 1 ? true: false,
weekklyTuesday: moment().weekday() === 2 ? true: false,
weekklyWednesday: moment().weekday() === 3 ? true: false,
weekklyThursday: moment().weekday() === 4 ? true: false,
weeklyFriday: moment().weekday() === 5 ? true: false,
weeklySaturday: moment().weekday() === 6 ? true: false,
isLoading: false,
errorMessageNumberOfWeeks: '',
};
//
this.onNumberOfWeeksChange = this.onNumberOfWeeksChange.bind(this);
this.onNumberOfOcurrencesChange = this.onNumberOfOcurrencesChange.bind(this);
this.onDataRangeOptionChange = this.onDataRangeOptionChange.bind(this);
this.onEndDateChange = this.onEndDateChange.bind(this);
this.onStartDateChange = this.onStartDateChange.bind(this);
this.onApplyRecurrence = this.onApplyRecurrence.bind(this);
this.onCheckboxSundayChange = this.onCheckboxSundayChange.bind(this);
this.onCheckboxMondayChange = this.onCheckboxMondayChange.bind(this);
this.onCheckboxTuesdayChange = this.onCheckboxTuesdayChange.bind(this);
this.onCheckboxWednesdayChange = this.onCheckboxWednesdayChange.bind(this);
this.onCheckboxThursdayChange = this.onCheckboxThursdayChange.bind(this);
this.onCheckboxFridayChange = this.onCheckboxFridayChange.bind(this);
this.onCheckboxSaturdayChange = this.onCheckboxSaturdayChange.bind(this);
this.spService = new spservices(this.props.context);
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onStartDateChange(date: Date) {
this.setState({ startDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onEndDateChange(date: Date) {
this.setState({ endDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onNumberOfWeeksChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
setTimeout(() => {
let errorMessage:string ='';
if (Number(value.trim()) == 0 || Number(value.trim()) > 255) {
value = '1 ';
errorMessage = 'Allowed values 1 to 255';
}
this.setState({ numberOfWeeks: value , errorMessageNumberOfWeeks: errorMessage });
this.applyRecurrence();
}, 2000);
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onNumberOfOcurrencesChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
setTimeout(() => {
this.setState({ numberOcurrences: value.trim().length > 0 ? value : "10 " });
this.applyRecurrence();
}, 2000);
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {IChoiceGroupOption} option
* @memberof EventRecurrenceInfoDaily
*/
private onDataRangeOptionChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectdateRangeOption: option.key,
disableNumberOcurrences: option.key == 'endAfter' ? false : true,
disableEndDate: option.key == 'endDate' ? false : true,
});
this.applyRecurrence();
}
private onPaternChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectPatern: option.key,
disableNumberOfWeeks: option.key == 'every' ? false : true,
});
this.applyRecurrence();
}
public async componentWillMount() {
// await this.load();
await this.load();
}
public async componentDidUpdate(prevProps: IEventRecurrenceInfoWeeklyProps, prevState: IEventRecurrenceInfoWeeklyState) {
}
private async load() {
let patern: any = {};
let dateRange: { repeatForever?: string, repeatInstances?: string, windowEnd?: Date } = {};
let weeklyPatern: { weekFrequency?: string, su?: boolean, mo?: boolean, tu?: boolean, we?: boolean, th?: boolean, fr?: boolean, sa?: boolean } = {};
if (this.props.recurrenceData) {
parseString(this.props.recurrenceData, { explicitArray: false }, (error, result) => {
if (result.recurrence.rule.repeat) {
patern = result.recurrence.rule.repeat;
}
//
if (result.recurrence.rule.repeatForever) {
dateRange = { repeatForever: result.recurrence.rule.repeatForever };
}
if (result.recurrence.rule.repeatInstances) {
dateRange = { repeatInstances: result.recurrence.rule.repeatInstances };
}
if (result.recurrence.rule.windowEnd) {
dateRange = { windowEnd: result.recurrence.rule.windowEnd };
}
});
// daily Patern
if (patern.weekly) {
weeklyPatern = patern.weekly.$.weekFrequency ? { weekFrequency: patern.weekly.$.weekFrequency } : { weekFrequency: 1 };
const weeklysu = patern.weekly.$.su ? true : false;
const weeklymo = patern.weekly.$.mo ? true : false;
const weeklytu = patern.weekly.$.tu ? true : false;
const weeklywe = patern.weekly.$.we ? true : false;
const weeklyth = patern.weekly.$.th ? true : false;
const weeklyfr = patern.weekly.$.fr ? true : false;
const weeklysa = patern.weekly.$.sa ? true : false;
weeklyPatern = { su: weeklysu, mo: weeklymo, tu: weeklytu, we: weeklywe, th: weeklyth, fr: weeklyfr, sa: weeklysa };
}
let selectDateRangeOption: string = 'noDate';
if (dateRange.repeatForever) {
selectDateRangeOption = 'noDate';
} else if (dateRange.repeatInstances) {
selectDateRangeOption = 'endAfter';
} else if (dateRange.windowEnd) {
selectDateRangeOption = 'endDate';
}
console.log(selectDateRangeOption, new Date(moment(dateRange.windowEnd).format('YYYY/MM/DD')));
// weekday patern
this.setState({
weeklySunday: weeklyPatern.su,
weeklyMonday: weeklyPatern.mo,
weekklyTuesday: weeklyPatern.tu,
weekklyWednesday: weeklyPatern.we,
weekklyThursday: weeklyPatern.th,
weeklyFriday: weeklyPatern.fr,
weeklySaturday: weeklyPatern.sa,
selectPatern: weeklyPatern.weekFrequency,
numberOfWeeks: weeklyPatern.weekFrequency ? weeklyPatern.weekFrequency : '1',
selectdateRangeOption: selectDateRangeOption,
numberOcurrences: dateRange.repeatInstances ? dateRange.repeatInstances : '1',
disableNumberOcurrences: dateRange.repeatInstances ? false : true,
endDate: dateRange.windowEnd ? new Date(moment(dateRange.windowEnd).format('YYYY/MM/DD')) : this.state.endDate,
disableEndDate: dateRange.windowEnd ? false : true,
isLoading: false,
});
}
await this.applyRecurrence();
}
private async onApplyRecurrence(ev: React.MouseEvent<HTMLButtonElement>) {
await this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.MouseEvent<HTMLButtonElement>} ev
* @memberof EventRecurrenceInfoDaily
*/
private async applyRecurrence() {
const siteTimeZoneHours: number = await this.spService.getSiteTimeZoneHours(this.props.siteUrl);
const eventDate = new Date(moment(this.state.startDate).add(siteTimeZoneHours, 'hours').toISOString());
const endDate = moment(this.state.endDate).add(siteTimeZoneHours, 'hours').toISOString();
let selectDateRangeOption;
switch (this.state.selectdateRangeOption) {
case 'noDate':
selectDateRangeOption = `<repeatForever>FALSE</repeatForever>`;
break;
case 'endAfter':
selectDateRangeOption = `<repeatInstances>${this.state.numberOcurrences.trim()}</repeatInstances>`;
break;
case 'endDate':
selectDateRangeOption = `<windowEnd>${endDate}</windowEnd>`;
break;
default:
break;
}
// test weekDays
let weekdays: string = '';
if (this.state.weeklySunday) {
weekdays = 'su="TRUE" ';
}
if (this.state.weeklyMonday) {
weekdays = `${weekdays} mo="TRUE"`;
}
if (this.state.weekklyTuesday) {
weekdays = `${weekdays} tu="TRUE"`;
}
if (this.state.weekklyWednesday) {
weekdays = `${weekdays} we="TRUE"`;
}
if (this.state.weekklyThursday) {
weekdays = `${weekdays} th="TRUE"`;
}
if (this.state.weeklyFriday) {
weekdays = `${weekdays} fr="TRUE"`;
}
if (this.state.weeklySaturday) {
weekdays = `${weekdays} sa="TRUE"`;
}
const recurrenceXML = `<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat>` +
`<weekly ${weekdays} weekFrequency="${this.state.numberOfWeeks.trim()}" /></repeat>${selectDateRangeOption}</rule></recurrence>`;
console.log(recurrenceXML);
this.props.returnRecurrenceData(this.state.startDate, recurrenceXML);
}
private async onCheckboxSundayChange(ev: React.FormEvent<HTMLElement>, isChecked: boolean) {
this.setState({ weeklySunday: isChecked });
await this.applyRecurrence();
}
private async onCheckboxMondayChange(ev: React.FormEvent<HTMLElement>, isChecked: boolean) {
this.setState({ weeklyMonday: isChecked });
await this.applyRecurrence();
}
private async onCheckboxTuesdayChange(ev: React.FormEvent<HTMLElement>, isChecked: boolean) {
this.setState({ weekklyTuesday: isChecked });
await this.applyRecurrence();
}
private async onCheckboxWednesdayChange(ev: React.FormEvent<HTMLElement>, isChecked: boolean) {
this.setState({ weekklyWednesday: isChecked });
await this.applyRecurrence();
}
private async onCheckboxThursdayChange(ev: React.FormEvent<HTMLElement>, isChecked: boolean) {
this.setState({ weekklyThursday: isChecked });
await this.applyRecurrence();
}
private async onCheckboxFridayChange(ev: React.FormEvent<HTMLElement>, isChecked: boolean) {
this.setState({ weeklyFriday: isChecked });
await this.applyRecurrence();
}
private async onCheckboxSaturdayChange(ev: React.FormEvent<HTMLElement>, isChecked: boolean) {
this.setState({ weeklySaturday: isChecked });
await this.applyRecurrence();
}
/**
*
*
* @returns {React.ReactElement<IEventRecurrenceInfoWeeklyProps>}
* @memberof EventRecurrenceInfoWeekly
*/
public render(): React.ReactElement<IEventRecurrenceInfoWeeklyProps> {
return (
<div >
{
<div>
<div style={{ display: 'inline-block', float: 'right', paddingTop: '10px', height: '40px' }}>
</div>
<div style={{ width: '100%', paddingTop: '10px' }}>
<Label>{strings.PaternLabel}</Label>
<div>
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '40px' } }}>{strings.every}</Label>
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '5px' } }}
mask="999"
maskChar=' '
errorMessage={this.state.errorMessageNumberOfWeeks}
value={this.state.numberOfWeeks}
onChange={this.onNumberOfWeeksChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '80px', paddingLeft: '10px' } }}>{strings.WeeksOnLabel}</Label>
</div>
<div style={{ marginTop: '10px' }}>
<Checkbox label="Sunday" className={styles.ckeckBoxInline} checked={this.state.weeklySunday} onChange={this.onCheckboxSundayChange} />
<Checkbox label="Monday" className={styles.ckeckBoxInline} checked={this.state.weeklyMonday} onChange={this.onCheckboxMondayChange} />
<Checkbox label="Tuesday" className={styles.ckeckBoxInline} checked={this.state.weekklyTuesday} onChange={this.onCheckboxTuesdayChange} />
<Checkbox label="Wednesday" className={styles.ckeckBoxInline} checked={this.state.weekklyWednesday} onChange={this.onCheckboxWednesdayChange} />
</div>
<div style={{ marginTop: '10px' }}>
<Checkbox label="Thursday" className={styles.ckeckBoxInline} checked={this.state.weekklyThursday} onChange={this.onCheckboxThursdayChange} />
<Checkbox label="Friday" className={styles.ckeckBoxInline} checked={this.state.weeklyFriday} onChange={this.onCheckboxFridayChange} />
<Checkbox label="Saturday" className={styles.ckeckBoxInline} checked={this.state.weeklySaturday} onChange={this.onCheckboxSaturdayChange} />
</div>
</div>
<div style={{ paddingTop: '22px' }}>
<Label>{strings.dateRangeLabel}</Label>
<div className={styles.dateRange}>
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingTop: '10px' }}>
<ChoiceGroup
selectedKey={this.state.selectdateRangeOption}
onChange={this.onDataRangeOptionChange}
options={[
{
key: 'noDate',
text: strings.noEndDate,
},
{
key: 'endDate',
text: strings.EndByLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>
</div>
);
}
},
{
key: 'endAfter',
text: strings.EndAfterLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="999"
maskChar=' '
value={this.state.numberOcurrences}
disabled={this.state.disableNumberOcurrences}
onChange={this.onNumberOfOcurrencesChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', paddingLeft: '10px' } }}>{strings.OcurrencesLabel}</Label>
</div>
);
}
},
]}
required={true}
/>
</div>
</div>
</div>
}
</div>
);
}
}

View File

@ -0,0 +1,9 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEventRecurrenceInfoWeeklyProps {
display:boolean;
recurrenceData: string;
startDate:Date;
context: WebPartContext;
siteUrl:string;
returnRecurrenceData: (startDate:Date,recurrenceData:string) => void;
}

View File

@ -0,0 +1,21 @@
export interface IEventRecurrenceInfoWeeklyState {
selectedKey:string;
selectPatern:string;
startDate: Date;
endDate:Date;
numberOcurrences:string;
numberOfWeeks:string;
disableNumberOfWeeks: boolean;
disableNumberOcurrences: boolean;
selectdateRangeOption:string;
disableEndDate:boolean;
weeklySunday:boolean;
weeklyMonday:boolean;
weekklyTuesday:boolean;
weekklyWednesday:boolean;
weekklyThursday:boolean;
weeklyFriday:boolean;
weeklySaturday:boolean;
isLoading:boolean;
errorMessageNumberOfWeeks:string;
}

View File

@ -0,0 +1,6 @@
.divWrraper {
border-width:1px;
border-color:#adadad;
padding: 20px;
border-style: solid;
}

View File

@ -0,0 +1,644 @@
import * as React from 'react';
import styles from './EventRecurrenceInfoYearly.module.scss';
import * as strings from 'CalendarWebPartStrings';
import { IEventRecurrenceInfoYearlyProps } from './IEventRecurrenceInfoYearlyProps';
import { IEventRecurrenceInfoYearlyState } from './IEventRecurrenceInfoYearlyState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as moment from 'moment';
import { parseString, Builder } from "xml2js";
import {
ChoiceGroup,
IChoiceGroupOption,
Dropdown,
IDropdownOption,
TextField,
SpinButton,
Label,
PrimaryButton,
MaskedTextField,
CommandBarButton, IButtonProps,
DefaultButton
} from 'office-ui-fabric-react';
import { Position } from 'office-ui-fabric-react/lib/utilities/positioning';
import { Root } from '@pnp/graph';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import spservices from '../../services/spservices';
const DayPickerStrings: IDatePickerStrings = {
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
goToToday: 'Go to today',
prevMonthAriaLabel: 'Go to previous month',
nextMonthAriaLabel: 'Go to next month',
prevYearAriaLabel: 'Go to previous year',
nextYearAriaLabel: 'Go to next year',
closeButtonAriaLabel: 'Close date picker'
};
/**
*
*
* @export
* @class EventRecurrenceInfoDaily
* @extends {React.Component<IEventRecurrenceInfoYearlyProps, IEventRecurrenceInfoYearlyState>}
*/
export class EventRecurrenceInfoYearly extends React.Component<IEventRecurrenceInfoYearlyProps, IEventRecurrenceInfoYearlyState> {
private spService: spservices = null;
public constructor(props) {
super(props);
this.onPaternChange = this.onPaternChange.bind(this);
this.state = {
selectedKey: 'daily',
selectPatern: 'yearly',
startDate: this.props.startDate ? this.props.startDate : moment().toDate(),
endDate: moment().endOf('month').toDate(),
numberOcurrences: '1',
disableDayOfMonth: false,
disableNumberOcurrences: true,
selectdateRangeOption: 'noDate',
disableEndDate: true,
selectedRecurrenceRule: 'yearly',
dayOfMonth: this.props.startDate ? moment(this.props.startDate).format('D') : moment().format('D'),
isLoading: false,
errorMessageDayOfMonth: '',
selectedWeekOrderMonth: 'first',
selectedWeekDay: 'day',
selectedMonth: moment().format('M'),
selectedYearlyByDayMonth: moment().format('M'),
};
//
this.onDayOfMonthChange = this.onDayOfMonthChange.bind(this);
this.onNumberOfOcurrencesChange = this.onNumberOfOcurrencesChange.bind(this);
this.onDataRangeOptionChange = this.onDataRangeOptionChange.bind(this);
this.onEndDateChange = this.onEndDateChange.bind(this);
this.onStartDateChange = this.onStartDateChange.bind(this);
this.onApplyRecurrence = this.onApplyRecurrence.bind(this);
this.onYearlyByDayMonthChange = this.onYearlyByDayMonthChange.bind(this);
this.onSelectedWeekDayChange = this.onSelectedWeekDayChange.bind(this);
this.onWeekOrderMonthChange = this.onWeekOrderMonthChange.bind(this);
this.onMonthChange = this.onMonthChange.bind(this);
this.spService = new spservices(this.props.context);
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onStartDateChange(date: Date) {
this.setState({ startDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {Date} date
* @memberof EventRecurrenceInfoDaily
*/
private onEndDateChange(date: Date) {
this.setState({ endDate: date });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onDayOfMonthChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
setTimeout(() => {
let errorMessage = '';
if (Number(value.trim()) < 1 || Number(value.trim()) > 31) {
value = '1 ';
errorMessage = 'Allowed values 1 to 31';
}
this.setState({ dayOfMonth: value, errorMessageDayOfMonth: errorMessage });
this.applyRecurrence();
}, 3000);
}
private onMonthChange(ev: React.SyntheticEvent<HTMLElement>, item: IDropdownOption) {
this.setState({ selectedMonth: item.key });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {string} value
* @memberof EventRecurrenceInfoDaily
*/
private onNumberOfOcurrencesChange(ev: React.SyntheticEvent<HTMLElement>, value: string) {
ev.preventDefault();
this.setState({ numberOcurrences: value });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {IChoiceGroupOption} option
* @memberof EventRecurrenceInfoDaily
*/
private onDataRangeOptionChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectdateRangeOption: option.key,
disableNumberOcurrences: option.key == 'endAfter' ? false : true,
disableEndDate: option.key == 'endDate' ? false : true,
});
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.SyntheticEvent<HTMLElement>} ev
* @param {IChoiceGroupOption} option
* @memberof EventRecurrenceInfoYearly
*/
private onPaternChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectPatern: option.key,
disableDayOfMonth: option.key == 'yearly' ? false : true,
});
this.applyRecurrence();
}
public async componentDidMount() {
}
public async componentWillMount() {
await this.load();
}
/**
*
*
* @private
* @param {React.FormEvent<HTMLDivElement>} ev
* @param {IDropdownOption} item
* @memberof EventRecurrenceInfoYearly
*/
private onWeekOrderMonthChange(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
this.setState({ selectedWeekOrderMonth: item.text });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.FormEvent<HTMLDivElement>} ev
* @param {IDropdownOption} item
* @memberof EventRecurrenceInfoYearly
*/
private onYearlyByDayMonthChange(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
this.setState({ selectedYearlyByDayMonth: item.key });
this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.FormEvent<HTMLDivElement>} ev
* @param {IDropdownOption} item
* @memberof EventRecurrenceInfoYearly
*/
private onSelectedWeekDayChange(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
this.setState({ selectedWeekDay: item.text });
this.applyRecurrence();
}
public async componentDidUpdate(prevProps: IEventRecurrenceInfoYearlyProps, prevState: IEventRecurrenceInfoYearlyState) {
}
/**
*
*
* @private
* @memberof EventRecurrenceInfoYearly
*/
private async load() {
let patern: any = {};
let dateRange: { repeatForever?: string, repeatInstances?: string, windowEnd?: Date } = {};
let yearlyPatern: { yearFrequency?: string, day?: string, month?: string } = {};
let yearlyByDayPatern: { yearFrequency?: string, weekdayOfMonth?: string, weekDay?: string, month?: string } = {};
let recurrenceRule: string;
if (this.props.recurrenceData) {
parseString(this.props.recurrenceData, { explicitArray: false }, (error, result) => {
if (result.recurrence.rule.repeat) {
patern = result.recurrence.rule.repeat;
}
//
if (result.recurrence.rule.repeatForever) {
dateRange = { repeatForever: result.recurrence.rule.repeatForever };
}
if (result.recurrence.rule.repeatInstances) {
dateRange = { repeatInstances: result.recurrence.rule.repeatInstances };
}
if (result.recurrence.rule.windowEnd) {
dateRange = { windowEnd: result.recurrence.rule.windowEnd };
}
});
// yearly Patern
if (patern.yearly) {
recurrenceRule = 'yearly';
if (patern.yearly.$.yearFrequency && patern.yearly.$.day) {
yearlyPatern = { yearFrequency: patern.yearly.$.yearFrequency, day: patern.yearly.$.day, month: patern.yearly.$.month };
}
}
// yearlyByDay Patern
if (patern.yearlyByDay) {
recurrenceRule = 'yearly';
let weekDay = 'day';
if (patern.yearlyByDay.$.su) weekDay = 'sunday';
if (patern.yearlyByDay.$.mo) weekDay = 'monday';
if (patern.yearlyByDay.$.tu) weekDay = 'tuesday';
if (patern.yearlyByDay.$.we) weekDay = 'wednesday';
if (patern.yearlyByDay.$.th) weekDay = 'thursday';
if (patern.yearlyByDay.$.fr) weekDay = 'friday';
if (patern.yearlyByDay.$.sa) weekDay = 'saturday';
if (patern.yearlyByDay.$.day) weekDay = 'day';
if (patern.yearlyByDay.$.weekday) weekDay = 'weekday';
if (patern.yearlyByDay.$.weekend_day) weekDay = 'weekdendday';
yearlyByDayPatern = {
yearFrequency: patern.yearlyByDay.$.yearFrequency,
weekdayOfMonth: patern.yearlyByDay.$.weekdayOfMonth,
weekDay: weekDay,
month: patern.yearlyByDay.$.month,
};
}
let selectDateRangeOption: string = 'noDate';
if (dateRange.repeatForever) {
selectDateRangeOption = 'noDate';
} else if (dateRange.repeatInstances) {
selectDateRangeOption = 'endAfter';
} else if (dateRange.windowEnd) {
selectDateRangeOption = 'endDate';
}
// weekday patern
this.setState({
selectedRecurrenceRule: recurrenceRule,
selectPatern: patern.yearly ? 'yearly' : 'yearlyByDay',
dayOfMonth: yearlyPatern.day ? yearlyPatern.day : '1',
selectedMonth: yearlyPatern.month ? yearlyPatern.month : moment().month(),
selectedYearlyByDayMonth: yearlyByDayPatern.month ? yearlyByDayPatern.month : moment().format('M'),
selectedWeekOrderMonth: yearlyByDayPatern.weekdayOfMonth ? yearlyByDayPatern.weekdayOfMonth : 'first',
selectedWeekDay: yearlyByDayPatern.weekDay ? yearlyByDayPatern.weekDay : 'day',
disableDayOfMonth: patern.yearly ? false : true,
selectdateRangeOption: selectDateRangeOption,
numberOcurrences: dateRange.repeatInstances ? dateRange.repeatInstances : '10',
disableNumberOcurrences: dateRange.repeatInstances ? false : true,
endDate: dateRange.windowEnd ? new Date(moment(dateRange.windowEnd).format('YYYY/MM/DD')) : this.state.endDate,
disableEndDate: dateRange.windowEnd ? false : true,
isLoading: false,
});
}
await this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.MouseEvent<HTMLButtonElement>} ev
* @memberof EventRecurrenceInfoYearly
*/
private async onApplyRecurrence(ev: React.MouseEvent<HTMLButtonElement>) {
await this.applyRecurrence();
}
/**
*
*
* @private
* @param {React.MouseEvent<HTMLButtonElement>} ev
* @memberof EventRecurrenceInfoDaily
*/
private async applyRecurrence() {
const siteTimeZoneHours: number = await this.spService.getSiteTimeZoneHours(this.props.siteUrl);
const eventDate = new Date(moment(this.state.startDate).add(siteTimeZoneHours, 'hours').toISOString());
const endDate = moment(this.state.endDate).add(siteTimeZoneHours, 'hours').toISOString();
let selectDateRangeOption;
switch (this.state.selectdateRangeOption) {
case 'noDate':
selectDateRangeOption = `<repeatForever>FALSE</repeatForever>`;
break;
case 'endAfter':
selectDateRangeOption = `<repeatInstances>${this.state.numberOcurrences}</repeatInstances>`;
break;
case 'endDate':
selectDateRangeOption = `<windowEnd>${endDate}</windowEnd>`;
break;
default:
break;
}
let recurrencePatern: string = '';
if (this.state.selectPatern == 'yearly') {
recurrencePatern = `<yearly yearFrequency="1" day="${this.state.dayOfMonth}" month="${this.state.selectedMonth}" /></repeat>${selectDateRangeOption}</rule></recurrence>`;
}
if (this.state.selectPatern == 'yearlyByDay') {
recurrencePatern = `<yearlyByDay weekdayOfMonth="${this.state.selectedWeekOrderMonth}" month="${this.state.selectedYearlyByDayMonth}"`;
switch (this.state.selectedWeekDay) {
case 'day':
recurrencePatern = recurrencePatern + `day="TRUE"`;
break;
case 'weekday':
recurrencePatern = recurrencePatern + `weekday="TRUE"`;
break;
case 'weekendday':
recurrencePatern = recurrencePatern + `weekend_day="TRUE"`;
break;
case 'sunday':
recurrencePatern = recurrencePatern + `su="TRUE"`;
break;
case 'monday':
recurrencePatern = recurrencePatern + `mo="TRUE"`;
break;
case 'tuesday':
recurrencePatern = recurrencePatern + `tu="TRUE"`;
break;
case 'wednesday':
recurrencePatern = recurrencePatern + `we="TRUE"`;
break;
case 'thursday':
recurrencePatern = recurrencePatern + `th="TRUE"`;
break;
case 'friday':
recurrencePatern = recurrencePatern + `fr="TRUE"`;
break;
case 'saturday':
recurrencePatern = recurrencePatern + `sa="TRUE"`;
break;
default:
break;
}
recurrencePatern = recurrencePatern + ` yearFrequency="1" /></repeat>${selectDateRangeOption}</rule></recurrence>`;
}
const recurrenceXML = `<recurrence><rule><firstDayOfWeek>su</firstDayOfWeek><repeat>` + recurrencePatern;
this.props.returnRecurrenceData(this.state.startDate, recurrenceXML);
}
/**
*
*
* @returns {React.ReactElement<IEventRecurrenceInfoDailyProps>}
* @memberof EventRecurrenceInfoDaily
*/
public render(): React.ReactElement<IEventRecurrenceInfoYearlyProps> {
return (
<div >
{
<div>
<div style={{ display: 'inline-block', float: 'right', paddingTop: '10px', height: '40px' }}>
</div>
<div style={{ width: '100%', paddingTop: '10px' }}>
<Label>{strings.PaternLabel}</Label>
<ChoiceGroup
selectedKey={this.state.selectPatern}
options={[
{
key: 'yearly',
text: strings.every,
ariaLabel: strings.every,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<div style={{ display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' }}>
<Dropdown
selectedKey={this.state.selectedMonth}
onChange={this.onMonthChange}
disabled={this.state.disableDayOfMonth}
options={[
{ key: '1', text: strings.January },
{ key: '2', text: strings.February },
{ key: '3', text: strings.March },
{ key: '4', text: strings.April },
{ key: '5', text: strings.May },
{ key: '6', text: strings.June },
{ key: '7', text: strings.July },
{ key: '8', text: strings.August },
{ key: '9', text: strings.September },
{ key: '10', text: strings.October },
{ key: '11', text: strings.November },
{ key: '12', text: strings.December },
]}
/>
</div>
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="99"
maskChar=' '
disabled={this.state.disableDayOfMonth}
value={this.state.dayOfMonth}
errorMessage={this.state.errorMessageDayOfMonth}
onChange={this.onDayOfMonthChange} />
</div>
);
}
},
{
key: 'yearlyByDay',
text: strings.theLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<div style={{ display: 'inline-block', verticalAlign: 'top', width: '80px', paddingLeft: '10px' }}>
<Dropdown
selectedKey={this.state.selectedWeekOrderMonth}
onChange={this.onWeekOrderMonthChange}
disabled={!this.state.disableDayOfMonth}
options={[
{ key: 'first', text: strings.firstLabel },
{ key: 'second', text: strings.secondLabel },
{ key: 'third', text: strings.thirdLabel },
{ key: 'fourth', text: strings.fourthLabel },
{ key: 'last', text: strings.lastLabel },
]}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '5px' }}>
<Dropdown
selectedKey={this.state.selectedWeekDay}
disabled={!this.state.disableDayOfMonth}
onChange={this.onSelectedWeekDayChange}
options={[
{ key: 'day', text: strings.dayLable },
{ key: 'weekday', text: strings.weekDayLabel },
{ key: 'weekendday', text: strings.weekEndDay },
{ key: 'sunday', text: strings.Sunday },
{ key: 'monday', text: strings.Monday },
{ key: 'tuesday', text: strings.Tuesday },
{ key: 'wednesday', text: strings.Wednesday },
{ key: 'thursday', text: strings.Thursday },
{ key: 'friday', text: strings.Friday },
{ key: 'saturday', text: strings.Saturday },
]}
/>
</div>
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '30px', paddingLeft: '10px' } }}>of</Label>
<div style={{ display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '5px' }}>
<Dropdown
selectedKey={this.state.selectedYearlyByDayMonth}
onChange={this.onYearlyByDayMonthChange}
disabled={!this.state.disableDayOfMonth}
options={[
{ key: '1', text: strings.January },
{ key: '2', text: strings.February },
{ key: '3', text: strings.March },
{ key: '4', text: strings.April },
{ key: '5', text: strings.May },
{ key: '6', text: strings.June },
{ key: '7', text: strings.July },
{ key: '8', text: strings.August },
{ key: '9', text: strings.September },
{ key: '10', text: strings.October },
{ key: '11', text: strings.November },
{ key: '12', text: strings.December },
]}
/>
</div>
</div>
);
}
}
]}
onChange={this.onPaternChange}
required={true}
/>
</div>
<div style={{ paddingTop: '22px' }}>
<Label>Date Range</Label>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: '35px', paddingTop: '10px' }}>
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingTop: '10px' }}>
<ChoiceGroup
selectedKey={this.state.selectdateRangeOption}
onChange={this.onDataRangeOptionChange}
options={[
{
key: 'noDate',
text: strings.noEndDate,
},
{
key: 'endDate',
text: strings.EndByLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<DatePicker
firstDayOfWeek={DayOfWeek.Sunday}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>
</div>
);
}
},
{
key: 'endAfter',
text: strings.EndAfterLabel,
onRenderField: (props, render) => {
return (
<div >
{render!(props)}
<MaskedTextField
styles={{ root: { display: 'inline-block', verticalAlign: 'top', width: '100px', paddingLeft: '10px' } }}
mask="999"
maskChar=' '
value={this.state.numberOcurrences}
disabled={this.state.disableNumberOcurrences}
onChange={this.onNumberOfOcurrencesChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', paddingLeft: '10px' } }}>{strings.OcurrencesLabel}</Label>
</div>
);
}
},
]}
required={true}
/>
</div>
</div>
</div>
}
</div>
);
}
}

View File

@ -0,0 +1,9 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEventRecurrenceInfoYearlyProps {
display:boolean;
recurrenceData: string;
startDate:Date;
context: WebPartContext;
siteUrl:string;
returnRecurrenceData: (startDate:Date,recurrenceData:string) => void;
}

View File

@ -0,0 +1,20 @@
export interface IEventRecurrenceInfoYearlyState {
selectedKey:string;
selectPatern:string;
startDate: Date;
endDate:Date;
numberOcurrences:string;
dayOfMonth:string;
disableDayOfMonth: boolean;
disableNumberOcurrences: boolean;
selectdateRangeOption:string;
disableEndDate:boolean;
selectedRecurrenceRule:string;
isLoading:boolean;
errorMessageDayOfMonth:string;
selectedWeekOrderMonth:string;
selectedWeekDay:string;
selectedMonth:string | number;
selectedYearlyByDayMonth: string | number;
}

View File

@ -1,17 +1,25 @@
export interface IEventData { export interface IEventData {
id?:number; Id?:number;
ID?:number;
title: string; title: string;
Description?: any; Description?: any;
location?:string; location?:string;
start: Date; EventDate: Date;
end: Date; EndDate: Date;
color?:string; color?:string;
ownerInitial?: string; ownerInitial?: string;
ownerPhoto?:string; ownerPhoto?:string;
ownerEmail?:string; ownerEmail?:string;
ownerName?:string; ownerName?:string;
allDayEvent?: boolean; fAllDayEvent?: boolean;
attendes?: number[]; attendes?: number[];
geolocation?: {Longitude:number, Latitude: number}; geolocation?: {Longitude:number, Latitude: number};
Category?: string; Category?: string;
Duration?: number;
RecurrenceData?:string;
fRecurrence?:string | boolean;
EventType?:string;
UID?:string;
RecurrenceID?: string;
MasterSeriesItemID?: string;
} }

View File

@ -0,0 +1,8 @@
export default class parseRecurrentEvent {
constructor();
parseEvents(events: any, start: any, end: any): any[];
formatString(str: any): any;
parseDate(date: any, allDay: any): any;
parseEvent(e: any, start: any, end: any): any[];
cloneObj(obj: any): any;
}

View File

@ -0,0 +1,401 @@
import * as moment from 'moment';
export default class parseRecurrentEvent {
private wEvents: any[] = [];
private full: any[] = [] ;
public parseEvents(events: any[], start: any, end: any) {
this.wEvents = events;
for (var i = 0; i < events.length; i++) {
end = null;
if (events[i].RecurrenceData.indexOf('<windowEnd>') != -1) {
let wDtEnd = events[i].RecurrenceData.substring(events[i].RecurrenceData.indexOf("<windowEnd>") + 11);
wDtEnd = wDtEnd.substring(0, wDtEnd.indexOf('<'));
end = moment(wDtEnd).toDate();
}
this.full = this.full.concat(this.parseEvent(events[i], start, end));
}
// remove deleted recurrences EventType = 3
this.full = this.full.filter( (el,j)=>{
if (el.EventType != '3'){
return el;
}
});
return this.full;
}
public RecurrenceExceptionExists(masterSeriesItemId, date) {
const found = this.wEvents.filter((el,i) => {
if (moment(el.RecurrenceID).isSame(moment(date)) && el.MasterSeriesItemID == masterSeriesItemId ) {
return el;
}
});
return found.length > 0 ? true : false;
}
//
public formatString(str: string) {
var arr = str.split("'");
str = arr.join('');
arr = str.split('"');
str = arr.join('');
arr = str.split('=');
str = arr.join(' ');
str.trim();
return str.split(' ');
}
public parseDate(date: any, allDay: any) {
if (typeof date == 'string') {
if (allDay) {
if (date.lastIndexOf('Z') == date.length - 1) {
var dt = date.substring(0, date.length - 1);
return new Date(dt);
}
}
else {
return new Date(date);
}
}
return date;
}
public parseEvent(e: any, start: any, end: any) {
if (e.fRecurrence == '0' || e.fRecurrence == '4') {
e.EventDate = new Date(this.parseDate(e.EventDate, e.fAllDayEvent));
e.EndDate = new Date(this.parseDate(e.EndDate, e.fAllDayEvent));
return [e];
}
else {
start = start || this.parseDate(e.EventDate, e.fAllDayEvent);
end = end || this.parseDate(e.EndDate, e.fAllDayEvent);
var er = [];
var wd = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa'];
var wom = ['first', 'second', 'third', 'fourth'];
var rTotal: any = 0;
var total: any = 0;
if (e.RecurrenceData.indexOf('<repeatInstances>') != -1) {
rTotal = e.RecurrenceData.substring(e.RecurrenceData.indexOf("<repeatInstances>") + 17);
rTotal = parseInt(rTotal.substring(0, rTotal.indexOf('<')));
}
if (e.RecurrenceData.indexOf("<daily ") != -1) {
var str = e.RecurrenceData.substring(e.RecurrenceData.indexOf("<daily "));
str = str.substring(7, str.indexOf('/>') - 1);
var arr = this.formatString(str);
if (arr.indexOf("dayFrequency") != -1) {
var frequency = parseInt(arr[arr.indexOf("dayFrequency") + 1]);
var loop = true;
var init = this.parseDate(e.EventDate, e.fAllDayEvent);
while (loop) {
total++;
if ((new Date(init)).getTime() >= start.getTime()) {
var ed = new Date(init);
ed.setSeconds(ed.getSeconds() + e.Duration);
var ni = this.cloneObj(e);
ni.EventDate = new Date(init);
if (!this.RecurrenceExceptionExists(e.Id, ni.EventDate)) {
ni.EndDate = ed;
ni.fRecurrence = false;
ni.Id = e.Id;
ni.ID = e.Id;
er.push(ni);
}
}
init.setDate(init.getDate() + frequency);
if ((new Date(init) > end) || (rTotal > 0 && rTotal <= total)) loop = false;
}
}
else if (arr.indexOf("weekday") != -1) {
e.RecurrenceData = e.RecurrenceData + "<weekly mo='TRUE' tu='TRUE' we='TRUE' th='TRUE' fr='TRUE' weekFrequency='1' />";//change from daily on every weekday to weekly on every weekday
}
}
if (e.RecurrenceData.indexOf("<weekly ") != -1) {
var str = e.RecurrenceData.substring(e.RecurrenceData.indexOf("<weekly "));
str = str.substring(8, str.indexOf('/>') - 1);
var arr = this.formatString(str);
var frequency = parseInt(arr[arr.indexOf("weekFrequency") + 1]);
var loop = true;
var init = this.parseDate(e.EventDate, e.fAllDayEvent);
var initDay = init.getDay();
while (loop) {
for (var i = initDay; i < 7; i++) {
if (arr.indexOf(wd[i]) != -1 && (rTotal > total || rTotal == 0)) {
total++;
if ((new Date(init)).getTime() >= start.getTime()) {
var nd: any = new Date(init);
nd.setDate(nd.getDate() + (i - initDay));
var ed = new Date(nd);
ed.setSeconds(ed.getSeconds() + e.Duration);
var ni = this.cloneObj(e);
ni.EventDate = new Date(nd);
if (!this.RecurrenceExceptionExists(e.Id, ni.EventDate)) {
ni.EndDate = ed;
ni.fRecurrence = false;
ni.Id = e.Id;
ni.ID = e.Id;
er.push(ni);
}
}
}
}
init.setDate(init.getDate() + ((7 * frequency) - initDay));
initDay = 0;
if ((new Date(init) > end) || (rTotal > 0 && rTotal <= total)) loop = false;
}
}
if (e.RecurrenceData.indexOf("<monthly ") != -1) {
var str = e.RecurrenceData.substring(e.RecurrenceData.indexOf("<monthly "));
str = str.substring(9, str.indexOf('/>') - 1);
var arr = this.formatString(str);
var frequency = parseInt(arr[arr.indexOf("monthFrequency") + 1]);
var loop = true;
var init = this.parseDate(e.EventDate, e.fAllDayEvent);
var day = parseInt(arr[arr.indexOf("day") + 1]);
while (loop) {
total++;
if ((new Date(init)).getTime() >= start.getTime()) {
var nd: any = new Date(init);
nd.setDate(day);
if (nd.getMonth() == init.getMonth()) {
var ed = new Date(nd);
ed.setSeconds(ed.getSeconds() + e.Duration);
var ni = this.cloneObj(e);
ni.EventDate = new Date(nd);
if (!this.RecurrenceExceptionExists(e.Id, ni.EventDate)) {
ni.EndDate = ed;
ni.fRecurrence = false;
ni.Id = e.Id;
ni.ID = e.Id;
er.push(ni);
}
}
}
init.setMonth(init.getMonth() + frequency);
if ((new Date(init) > end) || (rTotal > 0 && rTotal <= total)) loop = false;
}
}
if (e.RecurrenceData.indexOf("<monthlyByDay ") != -1) {
var str = e.RecurrenceData.substring(e.RecurrenceData.indexOf("<monthlyByDay "));
str = str.substring(14, str.indexOf('/>') - 1);
var arr = this.formatString(str);
var frequency = parseInt(arr[arr.indexOf("monthFrequency") + 1]);
var loop = true;
var init = this.parseDate(e.EventDate, e.fAllDayEvent);
var weekdayOfMonth = arr[arr.indexOf("weekdayOfMonth") + 1];
var temp: any = new Date();
while (loop) {
total++;
if ((new Date(init)).getTime() >= start.getTime()) {
var nd: any = new Date(init);
nd.setDate(1); //set to first day of month
if (arr.indexOf("weekday") != -1) { //find first weekday - if not saturday or sunday, then current date is a weekday
if (nd.getDay() == 0) nd.setDate(nd.getDate() + 1);// add one day to sunday
else if (nd.getDay() == 6) nd.setDate(nd.getDate() + 2); //add two days to saturday
if (weekdayOfMonth == 'last') {
while (nd.getMonth() == init.getMonth()) {
temp = new Date(nd);
if (nd.getDay() == 5) nd.setDate(nd.getDate() + 3); //if the current date is friday, add three days to get to monday
else nd.setDate(nd.getDate() + 1); //otherwise, just add one day
}
nd = new Date(temp);
}
else {
for (var i: any = 0; i < wom.indexOf(weekdayOfMonth); i++) {
if (nd.getDay() == 5) nd.setDate(nd.getDate() + 3); //if the current date is friday, add three days to get to monday
else nd.setDate(nd.getDate() + 1); //otherwise, just add one day
}
}
}
else if (arr.indexOf("weekend_day") != -1) { //find first weekend day
if (nd.getDay() != 0 && nd.getDay() != 6) nd.setDate(nd.getDate() + (6 - nd.getDay())); //if not saturday or sunday, then add days to get to saturday
if (weekdayOfMonth == 'last') {
while (nd.getMonth() == init.getMonth()) {
temp = new Date(nd);
if (nd.getDay() == 0) nd.setDate(nd.getDate() + 6); //if the current date is sunday, add six days to get to saturday
else nd.setDate(nd.getDate() + 1); //otherwise, just add one day
}
nd = new Date(temp);
}
else {
for (var i: any = 0; i < wom.indexOf(weekdayOfMonth); i++) {
if (nd.getDay() == 0) nd.setDate(nd.getDate() + 6); //if the current date is sunday, add six days to get to saturday
else nd.setDate(nd.getDate() + 1); //otherwise, just add one day
}
}
}
else if (arr.indexOf("day") != -1) {//just looking for the Nth day in the month...
if (weekdayOfMonth == 'last') {
nd.setMonth(nd.getMonth() + 1);
nd.setDate(0);
}
else nd.setDate(nd.getDate() + (wom.indexOf(weekdayOfMonth))); //now add days to get to the Nth instance of this day
}
else {
for (var i: any = 0; i < wd.length; i++) { //get first instance of the specified day
if (arr.indexOf(wd[i]) != -1) {
if (nd.getDay() > i) nd.setDate(nd.getDate() + (7 - (nd.getDay() - i)));
else nd.setDate(nd.getDate() + (i - nd.getDay() ));
}
}
if (weekdayOfMonth == 'last') {
while (nd.getMonth() == init.getMonth()) {
temp = new Date(nd);
nd.setDate(nd.getDate() + 7); //add a week to each instance to get the Nth instance
}
nd = new Date(temp);
}
else {
for (var i: any = 0; i < wom.indexOf(weekdayOfMonth); i++) {
nd.setDate(nd.getDate() + 7); //add a week to each instance to get the Nth instance
console.log(nd);
}
}
}
if (nd.getMonth() == init.getMonth()) { //make sure the new date calculated actually falls within the current month (sometimes there may be no 4th instance of a day)
var ed = new Date(nd);
ed.setSeconds(ed.getSeconds() + e.Duration);
var ni = this.cloneObj(e);
ni.EventDate = new Date(nd);
if (!this.RecurrenceExceptionExists(e.Id, ni.EventDate)) {
ni.EndDate = ed;
ni.fRecurrence = false;
ni.Id = e.Id;
ni.ID = e.Id;
er.push(ni);
}
}
}
init.setMonth(init.getMonth() + frequency);
if ((new Date(init) > end) || (rTotal > 0 && rTotal <= total)) loop = false;
}
}
if (e.RecurrenceData.indexOf("<yearly ") != -1) {
var str = e.RecurrenceData.substring(e.RecurrenceData.indexOf("<yearly "));
str = str.substring(8, str.indexOf('/>') - 1);
var arr = this.formatString(str);
var frequency = parseInt(arr[arr.indexOf("yearFrequency") + 1]);
var loop = true;
var init = this.parseDate(e.EventDate, e.fAllDayEvent);
var month = (parseInt(arr[arr.indexOf("month") + 1]) - 1);
var day = parseInt(arr[arr.indexOf("day") + 1]);
while (loop) {
var nd: any = new Date(init);
nd.setMonth(month);
nd.setDate(day);
if ((new Date(init)).getTime() <= nd.getTime()) {
total++;
if ((new Date(init)).getTime() >= start.getTime()) {
var ed = new Date(nd);
ed.setSeconds(ed.getSeconds() + e.Duration);
var ni = this.cloneObj(e);
ni.EventDate = new Date(nd);
if (!this.RecurrenceExceptionExists(e.Id, ni.EventDate)) {
ni.EndDate = ed;
ni.fRecurrence = false;
ni.Id = e.Id;
ni.ID = e.Id;
er.push(ni);
}
}
}
init.setFullYear(init.getFullYear() + frequency);
if ((new Date(init) > end) || (rTotal > 0 && rTotal <= total)) loop = false;
}
}
if (e.RecurrenceData.indexOf("<yearlyByDay ") != -1) {
var str = e.RecurrenceData.substring(e.RecurrenceData.indexOf("<yearlyByDay "));
str = str.substring(13, str.indexOf('/>') - 1);
var arr = this.formatString(str);
var frequency = parseInt(arr[arr.indexOf("yearFrequency") + 1]);
var loop = true;
var init = this.parseDate(e.EventDate, e.fAllDayEvent);
var month = (parseInt(arr[arr.indexOf("month") + 1]) - 1);
var weekdayOfMonth = arr[arr.indexOf("weekdayOfMonth") + 1];
var day = 0;
for (var i: any = 0; i < wd.length; i++) {
if (arr.indexOf(wd[i]) != -1) {
if (arr[arr.indexOf(wd[i]) + 1].toLowerCase() == 'true') day = i;
}
}
while (loop) {
var nd: any = new Date(init);
nd.setMonth(month);
if ((new Date(init)).getTime() <= nd.getTime()) {
total++;
if ((new Date(init)).getTime() >= start.getTime()) {
nd.setDate(1);
var dayOfMonth = nd.getDay();
if (day < dayOfMonth) nd.setDate(nd.getDate() + ((7 - dayOfMonth) + day)); //first instance of this day in the selected month
else nd.setDate(nd.getDate() + (day - dayOfMonth));
if (weekdayOfMonth == 'last') {
var temp: any = new Date(nd);
while (temp.getMonth() == month) {
nd = new Date(temp);
temp.setDate(temp.getDate() + 7); //loop from first instance of month to last instance of month
}
}
else {
nd.setDate(nd.getDate() + (7 * (wom.indexOf(weekdayOfMonth))));
}
if (nd.getMonth() == month) {
var ed = new Date(nd);
ed.setSeconds(ed.getSeconds() + e.Duration);
var ni = this.cloneObj(e);
ni.EventDate = new Date(nd);
if (!this.RecurrenceExceptionExists(e.Id, ni.EventDate)) {
ni.EndDate = ed;
ni.fRecurrence = false;
ni.Id = e.Id;
ni.ID = e.Id;
er.push(ni);
}
}
}
}
init.setFullYear(init.getFullYear() + frequency);
init.setMonth(month);
init.setDate(1);
if ((new Date(init) > end) || (rTotal > 0 && rTotal <= total)) loop = false;
}
}
return er;
} //end recurrence check
}
public cloneObj(obj: any): any {
var copy: any;
if (null == obj || "object" != typeof obj) return obj;
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
if (obj instanceof Array) {
copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = this.cloneObj(obj[i]);
}
return copy;
}
if (obj instanceof Object) {
copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = this.cloneObj(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj! Its type isn't supported.");
}
}

View File

@ -8,14 +8,13 @@ import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions, HttpClient, M
import * as $ from 'jquery'; import * as $ from 'jquery';
import { IEventData } from './IEventData'; import { IEventData } from './IEventData';
import { registerDefaultFontFaces } from "@uifabric/styling"; import { registerDefaultFontFaces } from "@uifabric/styling";
import { EventArgs } from "@microsoft/sp-core-library";
import * as moment from 'moment'; import * as moment from 'moment';
import { SiteUser } from "@pnp/sp/src/siteusers"; import { SiteUser } from "@pnp/sp/src/siteusers";
import { IUserPermissions } from './IUserPermissions'; import { IUserPermissions } from './IUserPermissions';
import { dateAdd } from "@pnp/common"; import { dateAdd } from "@pnp/common";
import { escape } from '@microsoft/sp-lodash-subset'; import { escape, update } from '@microsoft/sp-lodash-subset';
import parseRecurrentEvent from './parseRecurrentEvent';
const ADMIN_ROLETEMPLATE_ID = "62e90394-69f5-4237-9190-012177145e10"; // Global Admin TemplateRoleId
// Class Services // Class Services
export default class spservices { export default class spservices {
@ -35,8 +34,6 @@ export default class spservices {
} }
// OnInit Function // OnInit Function
private async onInit() { private async onInit() {
//this.appCatalogUrl = await this.getAppCatalogUrl();
} }
/** /**
@ -46,9 +43,9 @@ export default class spservices {
* @returns {Promise<number>} * @returns {Promise<number>}
* @memberof spservices * @memberof spservices
*/ */
private async getSiteTimeZoneHoursToUtc(siteUrl: string): Promise<number> { public async getSiteTimeZoneHours(siteUrl: string): Promise<number> {
let numberHours: number = 0; let numberHours: number = 0;
let siteTimeZoneHoursToUTC: any; let siteTimeZoneHours: any;
let siteTimeZoneBias: number; let siteTimeZoneBias: number;
let siteTimeZoneDaylightBias: number; let siteTimeZoneDaylightBias: number;
let currentDateTimeOffSet: number = new Date().getTimezoneOffset() / 60; let currentDateTimeOffSet: number = new Date().getTimezoneOffset() / 60;
@ -86,26 +83,85 @@ export default class spservices {
try { try {
const web = new Web(siteUrl); const web = new Web(siteUrl);
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl); const siteTimeZoneHours: number = await this.getSiteTimeZoneHours(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.add({ results = await web.lists.getById(listId).items.add({
Title: newEvent.title, Title: newEvent.title,
Description: newEvent.Description, Description: newEvent.Description,
Geolocation: newEvent.geolocation, Geolocation: newEvent.geolocation,
ParticipantsPickerId: { results: newEvent.attendes }, ParticipantsPickerId: { results: newEvent.attendes },
EventDate: new Date(moment(newEvent.start).add(siteTimeZoneHoursToUTC, 'hours').toISOString()), EventDate: new Date(moment(newEvent.EventDate).add(siteTimeZoneHours, 'hours').toISOString()),
EndDate: new Date(moment(newEvent.end).add(siteTimeZoneHoursToUTC, 'hours').toISOString()), EndDate: new Date(moment(newEvent.EndDate).add(siteTimeZoneHours, 'hours').toISOString()),
Location: newEvent.location, Location: newEvent.location,
fAllDayEvent: false, fAllDayEvent: false,
fRecurrence: false, fRecurrence: newEvent.fRecurrence,
Category: newEvent.Category, Category: newEvent.Category,
EventType: newEvent.EventType,
UID: newEvent.UID,
RecurrenceData: newEvent.RecurrenceData ? await this.deCodeHtmlEntities(newEvent.RecurrenceData) : "",
MasterSeriesItemID: newEvent.MasterSeriesItemID,
RecurrenceID: newEvent.RecurrenceID ? moment(newEvent.RecurrenceID).add(siteTimeZoneHours, 'hours').toISOString() : undefined,
}); });
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
} }
return results; return results;
} }
/**
*
*
* @param {string} siteUrl
* @param {string} listId
* @param {number} eventId
* @returns {Promise<IEventData>}
* @memberof spservices
*/
public async getEvent(siteUrl: string, listId: string, eventId: number): Promise<IEventData> {
let returnEvent: IEventData = undefined;
try {
const siteTimeZoneHours: number = await this.getSiteTimeZoneHours(siteUrl);
const web = new Web(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
const event = await web.lists.getById(listId).items.usingCaching().getById(eventId)
.select("RecurrenceID", "MasterSeriesItemID", "Id", "ID", "ParticipantsPickerId", "EventType", "Title", "Description", "EventDate", "EndDate", "Location", "Author/SipAddress", "Author/Title", "Geolocation", "fAllDayEvent", "fRecurrence", "RecurrenceData", "RecurrenceData", "Duration", "Category", "UID")
.expand("Author")
.get();
returnEvent = {
Id: event.ID,
ID: event.ID,
EventType: event.EventType,
title: await this.deCodeHtmlEntities(event.Title),
Description: event.Description ? event.Description : '',
EventDate: new Date(moment(event.EventDate).subtract((siteTimeZoneHours), 'hour').toISOString()),
EndDate: new Date(moment(event.EndDate).subtract(siteTimeZoneHours, 'hour').toISOString()),
location: event.Location,
ownerEmail: event.Author.SipAddress,
ownerPhoto: "",
ownerInitial: '',
color: '',
ownerName: event.Author.Title,
attendes: event.ParticipantsPickerId,
fAllDayEvent: false,
geolocation: { Longitude: event.Geolocation ? event.Geolocation.Longitude : 0, Latitude: event.Geolocation ? event.Geolocation.Latitude : 0 },
Category: event.Category,
Duration: event.Duration,
UID: event.UID,
RecurrenceData: event.RecurrenceData ? await this.deCodeHtmlEntities(event.RecurrenceData) : "",
fRecurrence: event.fRecurrence,
RecurrenceID: event.RecurrenceID,
MasterSeriesItemID: event.MasterSeriesItemID,
};
} catch (error) {
return Promise.reject(error);
}
return returnEvent;
}
/** /**
* *
* @param {IEventData} newEvent * @param {IEventData} newEvent
@ -118,21 +174,28 @@ export default class spservices {
let results = null; let results = null;
try { try {
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl); // delete all recursive extentions before update recurrence event
if (updateEvent.EventType.toString() == "1") await this.deleteRecurrenceExceptions(updateEvent, siteUrl, listId);
const siteTimeZoneHours: number = await this.getSiteTimeZoneHours(siteUrl);
const web = new Web(siteUrl); const web = new Web(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId" //"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.getById(updateEvent.id).update({ results = await web.lists.getById(listId).items.getById(updateEvent.Id).update({
Title: updateEvent.title, Title: updateEvent.title,
Description: updateEvent.Description, Description: updateEvent.Description,
Geolocation: updateEvent.geolocation, Geolocation: updateEvent.geolocation,
ParticipantsPickerId: { results: updateEvent.attendes }, ParticipantsPickerId: { results: updateEvent.attendes },
EventDate: new Date(moment(updateEvent.start).add(siteTimeZoneHoursToUTC, 'hours').toISOString()), EventDate: new Date(moment(updateEvent.EventDate).add(siteTimeZoneHours, 'hours').toISOString()),
EndDate: new Date(moment(updateEvent.end).add(siteTimeZoneHoursToUTC, 'hours').toISOString()), EndDate: new Date(moment(updateEvent.EndDate).add(siteTimeZoneHours, 'hours').toISOString()),
Location: updateEvent.location, Location: updateEvent.location,
fAllDayEvent: false, fAllDayEvent: false,
fRecurrence: false, fRecurrence: updateEvent.fRecurrence,
Category: updateEvent.Category, Category: updateEvent.Category,
UID: updateEvent.UID,
RecurrenceData: updateEvent.RecurrenceData ? await this.deCodeHtmlEntities(updateEvent.RecurrenceData) : "",
EventType: updateEvent.EventType,
MasterSeriesItemID: updateEvent.MasterSeriesItemID,
}); });
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
@ -140,6 +203,25 @@ export default class spservices {
return results; return results;
} }
public async deleteRecurrenceExceptions(event: IEventData, siteUrl: string, listId: string) {
let results = null;
try {
const web = new Web(siteUrl);
results = await web.lists.getById(listId).items
.select('Id')
.filter(`EventType eq '3' or EventType eq '4' and MasterSeriesItemID eq '${event.Id}' `)
.get();
if (results && results.length > 0) {
for (const recurrenceException of results) {
await web.lists.getById(listId).items.getById(recurrenceException.Id).delete();
}
}
} catch (error) {
return Promise.reject(error);
}
return;
}
/** /**
* *
* @param {IEventData} event * @param {IEventData} event
@ -148,17 +230,44 @@ export default class spservices {
* @returns * @returns
* @memberof spservices * @memberof spservices
*/ */
public async deleteEvent(event: IEventData, siteUrl: string, listId: string) { public async deleteEvent(event: IEventData, siteUrl: string, listId: string, recurrenceSeriesEdited: boolean) {
let results = null; let results = null;
try { try {
const web = new Web(siteUrl); const web = new Web(siteUrl);
// Exception Recurrence eventtype = 4 ? update to deleted Recurrence eventtype=3
switch (event.EventType.toString()) {
case '4': // Exception Recurrence Event
results = await web.lists.getById(listId).items.getById(event.Id).update({
Title: `Delete: ${event.title}`,
EventType: '3',
});
break;
case '1': // recurrence Event
// if delete is a main recrrence delete all recurrences and main recurrence
if (recurrenceSeriesEdited) {
// delete execptions if exists before delete recurrence event
await this.deleteRecurrenceExceptions(event, siteUrl, listId);
await web.lists.getById(listId).items.getById(event.Id).delete();
} else {
// delete a single recurrence Exception. add new entry with eventtype 3
event.RecurrenceID = event.EventDate.toString();
event.MasterSeriesItemID = event.ID.toString();
event.fRecurrence = true;
event.EventType = '3';
await this.addEvent(event, siteUrl, listId);
}
break;
case '0': // normal Event
await web.lists.getById(listId).items.getById(event.Id).delete();
break;
}
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.getById(event.id).delete();
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
} }
return results; return;
} }
/** /**
* *
@ -218,7 +327,7 @@ export default class spservices {
public async getUserProfilePictureUrl(loginName: string) { public async getUserProfilePictureUrl(loginName: string) {
let results: any = null; let results: any = null;
try { try {
results = await sp.profiles.getPropertiesFor(loginName); results = await sp.profiles.usingCaching().getPropertiesFor(loginName);
} catch (error) { } catch (error) {
results = null; results = null;
} }
@ -241,12 +350,13 @@ export default class spservices {
try { try {
const web = new Web(siteUrl); const web = new Web(siteUrl);
const userEffectivePermissions = await web.lists.getById(listId).effectiveBasePermissions.get(); const userEffectivePermissions = await web.lists.getById(listId).effectiveBasePermissions.get();
// chaeck user permissions // ...
hasPermissionAdd = sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.AddListItems); hasPermissionAdd = sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.AddListItems);
hasPermissionEdit =sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.EditListItems);
hasPermissionDelete = sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.DeleteListItems); hasPermissionDelete = sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.DeleteListItems);
hasPermissionEdit = sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.EditListItems);
hasPermissionView = sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.ViewListItems); hasPermissionView = sp.web.lists.getById(listId).hasPermissions(userEffectivePermissions, PermissionKind.ViewListItems);
userPermissions = { hasPermissionAdd: hasPermissionAdd, hasPermissionEdit: hasPermissionEdit, hasPermissionDelete: hasPermissionDelete, hasPermissionView: hasPermissionView }; userPermissions = { hasPermissionAdd: hasPermissionAdd, hasPermissionEdit: hasPermissionEdit, hasPermissionDelete: hasPermissionDelete, hasPermissionView: hasPermissionView };
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);
} }
@ -289,6 +399,7 @@ export default class spservices {
for (var i = 0; i < 6; i++) { for (var i = 0; i < 6; i++) {
var x = Math.round(Math.random() * 14); var x = Math.round(Math.random() * 14);
var y = hexValues[x]; var y = hexValues[x];
newColor += y; newColor += y;
} }
@ -344,7 +455,7 @@ export default class spservices {
} }
try { try {
// Get Regional Settings TimeZone Hours to UTC // Get Regional Settings TimeZone Hours to UTC
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl); const siteTimeZoneHours: number = await this.getSiteTimeZoneHours(siteUrl);
// Get Category Field Choices // Get Category Field Choices
const categoryDropdownOption = await this.getChoiceFieldOptions(siteUrl, listId, 'Category'); const categoryDropdownOption = await this.getChoiceFieldOptions(siteUrl, listId, 'Category');
let categoryColor: { category: string, color: string }[] = []; let categoryColor: { category: string, color: string }[] = [];
@ -353,13 +464,12 @@ export default class spservices {
} }
const web = new Web(siteUrl); const web = new Web(siteUrl);
const results = await web.lists.getById(listId).renderListDataAsStream( const results = await web.lists.getById(listId).usingCaching().renderListDataAsStream(
{ {
DatesInUtc: true, 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> ViewXml: `<View><ViewFields><FieldRef Name='RecurrenceData'/><FieldRef Name='Duration'/><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'/><FieldRef Name='EventType'/><FieldRef Name='UID' /><FieldRef Name='fRecurrence' /></ViewFields>
<Query> <Query>
<Where> <Where>
<And>
<And> <And>
<Geq> <Geq>
<FieldRef Name='EventDate' /> <FieldRef Name='EventDate' />
@ -370,11 +480,6 @@ export default class spservices {
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventEndDate).format('YYYY-MM-DD')}</Value> <Value IncludeTimeValue='false' Type='DateTime'>${moment(eventEndDate).format('YYYY-MM-DD')}</Value>
</Leq> </Leq>
</And> </And>
<Eq>
<FieldRef Name='fRecurrence' />
<Value Type='Recurrence'>0</Value>
</Eq>
</And>
</Where> </Where>
</Query> </Query>
<RowLimit Paged=\"FALSE\">2000</RowLimit> <RowLimit Paged=\"FALSE\">2000</RowLimit>
@ -383,7 +488,8 @@ export default class spservices {
); );
if (results && results.Row.length > 0) { if (results && results.Row.length > 0) {
for (const event of results.Row) { let event: any = '';
for (event of results.Row) {
const initialsArray: string[] = event.Author[0].title.split(' '); const initialsArray: string[] = event.Author[0].title.split(' ');
const initials: string = initialsArray[0].charAt(0) + initialsArray[initialsArray.length - 1].charAt(0); 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 userPictureUrl = await this.getUserProfilePictureUrl(`i:0#.f|membership|${event.Author[0].email}`);
@ -399,28 +505,40 @@ export default class spservices {
attendees.push(parseInt(attendee.id)); attendees.push(parseInt(attendee.id));
} }
events.push({ events.push({
id: event.ID, Id: event.ID,
ID: event.ID,
EventType: event.EventType,
title: await this.deCodeHtmlEntities(event.Title), title: await this.deCodeHtmlEntities(event.Title),
Description: event.Description, Description: event.Description,
// start: moment(event.EventDate).utc().toDate().setUTCMinutes(this.siteTimeZoneOffSet),
start: new Date(moment(event.EventDate).subtract((siteTimeZoneHoursToUTC), 'hour').toISOString()), EventDate: new Date(moment(event.EventDate).subtract((siteTimeZoneHours), 'hour').toISOString()),
// end: new Date(moment(event.EndDate).toLocaleString()),
end: new Date(moment(event.EndDate).subtract(siteTimeZoneHoursToUTC, 'hour').toISOString()), EndDate: new Date(moment(event.EndDate).subtract(siteTimeZoneHours, 'hour').toISOString()),
location: event.Location, location: event.Location,
ownerEmail: event.Author[0].email, ownerEmail: event.Author[0].email,
ownerPhoto: userPictureUrl ? ownerPhoto: userPictureUrl ?
`https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${event.Author[0].email}&UA=0&size=HR96x96` : '', `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${event.Author[0].email}&UA=0&size=HR96x96` : '',
ownerInitial: initials, ownerInitial: initials,
// color: await this.colorGenerate(), color: CategoryColorValue.length > 0 ? CategoryColorValue[0].color : '#1a75ff', // blue default
color: CategoryColorValue.length > 0 ? CategoryColorValue[0].color : await this.colorGenerate,
ownerName: event.Author[0].title, ownerName: event.Author[0].title,
attendes: attendees, attendes: attendees,
allDayEvent: false, fAllDayEvent: false,
geolocation: { Longitude: parseFloat(geolocation[0]), Latitude: parseFloat(geolocation[1]) }, geolocation: { Longitude: parseFloat(geolocation[0]), Latitude: parseFloat(geolocation[1]) },
Category: event.Category Category: event.Category,
Duration: event.Duration,
RecurrenceData: event.RecurrenceData ? await this.deCodeHtmlEntities(event.RecurrenceData) : "",
fRecurrence: event.fRecurrence,
RecurrenceID: event.RecurrenceID ? moment(event.RecurrenceID).subtract(siteTimeZoneHours, 'hour').toISOString() : undefined,
MasterSeriesItemID: event.MasterSeriesItemID,
UID: event.UID.replace("{", "").replace("}", ""),
}); });
} }
let parseEvt: parseRecurrentEvent = new parseRecurrentEvent();
events = parseEvt.parseEvents(events, null, null);
} }
// Return Data // Return Data
return events; return events;
@ -441,7 +559,7 @@ export default class spservices {
let regionalSettings: RegionalSettings; let regionalSettings: RegionalSettings;
try { try {
const web = new Web(siteUrl); const web = new Web(siteUrl);
regionalSettings = await web.regionalSettings.timeZone.get(); regionalSettings = await web.regionalSettings.timeZone.usingCaching().get();
} catch (error) { } catch (error) {
return Promise.reject(error); return Promise.reject(error);

View File

@ -12,7 +12,8 @@
"requiresCustomScript": false, "requiresCustomScript": false,
"supportedHosts": [ "supportedHosts": [
"SharePointWebPart", "SharePointWebPart",
"TeamsTab" "TeamsTab",
"SharePointFullPage"
], ],
"preconfiguredEntries": [ "preconfiguredEntries": [
{ {

View File

@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDom from 'react-dom'; import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library'; import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart, PropertyPaneHorizontalRule } from '@microsoft/sp-webpart-base'; import { BaseClientSideWebPart, PropertyPaneHorizontalRule } from '@microsoft/sp-webpart-base';
import { import {
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
@ -66,7 +65,7 @@ export default class CalendarWebPart extends BaseClientSideWebPart<ICalendarWebP
public async onInit(): Promise<void> { public async onInit(): Promise<void> {
this.spService = new spservices(this.context); this.spService = new spservices(this.context);
this.properties.siteUrl = this.context.pageContext.site.absoluteUrl; this.properties.siteUrl = this.properties.siteUrl ? this.properties.siteUrl : this.context.pageContext.site.absoluteUrl;
if (!this.properties.eventStartDate){ if (!this.properties.eventStartDate){
this.properties.eventStartDate = { value: moment().subtract(2,'years').startOf('month').toDate(), displayValue: moment().format('ddd MMM MM YYYY')}; this.properties.eventStartDate = { value: moment().subtract(2,'years').startOf('month').toDate(), displayValue: moment().format('ddd MMM MM YYYY')};
} }
@ -75,9 +74,12 @@ export default class CalendarWebPart extends BaseClientSideWebPart<ICalendarWebP
} }
if (this.properties.siteUrl && !this.properties.list) { if (this.properties.siteUrl && !this.properties.list) {
const _lists = await this.loadLists(); const _lists = await this.loadLists();
if ( _lists.length > 0 ){
this.lists = _lists; this.lists = _lists;
this.properties.list = this.lists.length > 0 ? this.lists[0].key.toString() : ''; this.properties.list = this.lists[0].key.toString();
} }
}
return Promise.resolve(); return Promise.resolve();
} }
@ -130,14 +132,16 @@ export default class CalendarWebPart extends BaseClientSideWebPart<ICalendarWebP
for (const list of results) { for (const list of results) {
_lists.push({ key: list.Id, text: list.Title }); _lists.push({ key: list.Id, text: list.Title });
} }
// push new item value
} catch (error) { } catch (error) {
this.errorMessage = `${error.message} - ${strings.PropPanelSiteUrlErrorMessage}` ; this.errorMessage = `${error.message} - please check if site url if valid.` ;
this.context.propertyPane.refresh(); this.context.propertyPane.refresh();
} }
return _lists; return _lists;
} }
/** /**
*
* *
* @private * @private
* @param {string} date * @param {string} date
@ -226,7 +230,7 @@ export default class CalendarWebPart extends BaseClientSideWebPart<ICalendarWebP
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue); super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
} }
} catch (error) { } catch (error) {
this.errorMessage = `${error.message} - ${strings.PropPanelSiteUrlErrorMessage}` ; this.errorMessage = `${error.message} - please check if site url if valid.` ;
this.context.propertyPane.refresh(); this.context.propertyPane.refresh();
} }
} }
@ -237,6 +241,8 @@ export default class CalendarWebPart extends BaseClientSideWebPart<ICalendarWebP
* @memberof CalendarWebPart * @memberof CalendarWebPart
*/ */
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
// EndDate and Start Date defualt values
return { return {
pages: [ pages: [
{ {

View File

@ -1,4 +1,21 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss'; @import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
@import './node_modules/spfx-uifabric-themes/office.theme.vars';
:export {
themeDark: $ms-color-themePrimary;
}
.eventStyleSetter {
background-color: white;
border-radius: 0px;
color: $ms-color-themePrimary;
opacity: 1;
border-width: '1.1px';
border-style: 'solid';
border-color: $ms-color-themePrimary;
border-left-width: '6px';
display: 'block';
}
.Documentcard { .Documentcard {

View File

@ -8,8 +8,10 @@ import * as moment from 'moment';
import * as strings from 'CalendarWebPartStrings'; import * as strings from 'CalendarWebPartStrings';
import 'react-big-calendar/lib/css/react-big-calendar.css'; import 'react-big-calendar/lib/css/react-big-calendar.css';
require('./calendar.css'); require('./calendar.css');
import { import { CommunicationColors , FluentCustomizations, FluentTheme } from '@uifabric/fluent-theme';
import {
Customizer,
IPersonaSharedProps, IPersonaSharedProps,
Persona, Persona,
PersonaSize, PersonaSize,
@ -132,7 +134,6 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
} }
/** /**
*
* *
* @param {*} error * @param {*} error
* @param {*} errorInfo * @param {*} errorInfo
@ -170,7 +171,7 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
previewImages: [ previewImages: [
{ {
// previewImageSrc: event.ownerPhoto, // previewImageSrc: event.ownerPhoto,
previewIconProps: { iconName: 'Calendar', styles: { root: { color: event.color } }, className: styles.previewEventIcon }, previewIconProps: { iconName: event.fRecurrence === '0' ? 'Calendar': 'RecurringEvent', styles: { root: { color: event.color } }, className: styles.previewEventIcon },
height: 43, height: 43,
} }
] ]
@ -195,14 +196,13 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
<div className={styles.DocumentCardDetails}> <div className={styles.DocumentCardDetails}>
<DocumentCardTitle title={event.title} shouldTruncate={true} className={styles.DocumentCardTitle} styles={{ root: { color: event.color} }} /> <DocumentCardTitle title={event.title} shouldTruncate={true} className={styles.DocumentCardTitle} styles={{ root: { color: event.color} }} />
</div> </div>
{ {
moment(event.start).format('YYYY/MM/DD') !== moment(event.end).format('YYYY/MM/DD') ? moment(event.EventDate).format('YYYY/MM/DD') !== moment(event.EndDate).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.EventDate).format('dddd')} - {moment(event.EndDate).format('dddd')} </span>
: :
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('dddd')} </span> <span className={styles.DocumentCardTitleTime}>{moment(event.EventDate).format('dddd')} </span>
} }
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('HH:mm')}H - {moment(event.end).format('HH:mm')}H</span> <span className={styles.DocumentCardTitleTime}>{moment(event.EventDate).format('HH:mm')}H - {moment(event.EndDate).format('HH:mm')}H</span>
<Icon iconName='MapPin' className={styles.locationIcon} style={{ color: event.color }} /> <Icon iconName='MapPin' className={styles.locationIcon} style={{ color: event.color }} />
<DocumentCardTitle <DocumentCardTitle
title={`${event.location}`} title={`${event.location}`}
@ -223,7 +223,6 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
}; };
return ( return (
<div style={{ height: 22 }}> <div style={{ height: 22 }}>
<HoverCard <HoverCard
cardDismissDelay={1000} cardDismissDelay={1000}
@ -273,15 +272,16 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
* @memberof Calendar * @memberof Calendar
*/ */
public eventStyleGetter(event, start, end, isSelected): any { public eventStyleGetter(event, start, end, isSelected): any {
let style: any = { let style: any = {
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: '0px', borderRadius: '0px',
opacity: 1, opacity: 1,
color: 'black', color: event.color,
borderWidth: '1.1px', borderWidth: '1.1px',
borderStyle: 'solid', borderStyle: 'solid',
borderColor: event.color, borderColor: event.color,
borderLeftWidth: '5px', borderLeftWidth: '6px',
display: 'block' display: 'block'
}; };
@ -298,7 +298,10 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
public render(): React.ReactElement<ICalendarProps> { public render(): React.ReactElement<ICalendarProps> {
return ( return (
<div className={styles.calendar}> <Customizer {...FluentCustomizations}>
<div className={styles.calendar} style={{backgroundColor: 'white', padding: '20px'}}>
<WebPartTitle displayMode={this.props.displayMode} <WebPartTitle displayMode={this.props.displayMode}
title={this.props.title} title={this.props.title}
updateProperty={this.props.updateProperty} /> updateProperty={this.props.updateProperty} />
@ -326,8 +329,8 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
localizer={localizer} localizer={localizer}
selectable selectable
events={this.state.eventData} events={this.state.eventData}
startAccessor="start" startAccessor="EventDate"
endAccessor="end" endAccessor="EndDate"
eventPropGetter={this.eventStyleGetter} eventPropGetter={this.eventStyleGetter}
onSelectSlot={this.onSelectSlot} onSelectSlot={this.onSelectSlot}
components={{ components={{
@ -367,6 +370,7 @@ export default class Calendar extends React.Component<ICalendarProps, ICalendarS
/> />
} }
</div> </div>
</Customizer>
); );
} }
} }

View File

@ -1,14 +1,34 @@
define([], function () { define([], function () {
return { return {
PropPanelSiteUrlErrorMessage:'Please verify if site url is valid', WeeksOnLabel: "week(s) on",
PaternLabel: "Patern",
OcurrencesLabel: "Ocurrences",
dateRangeLabel: "Date Range",
weekEndDay: "Weekend Day",
weekDayLabel: "Weekday",
lastLabel: "last",
fourthLabel: "fourth",
thirdLabel: "third",
secondLabel: "second",
firstLabel: "first",
theLabel: "the",
MonthsLabel: "month(s)",
ofEveryLabel: "of every ",
AllowedValues1to12Label: "Allowed values 1 to 12",
noEndDate: "no end date",
everyweekdays: "every weekdays",
days: "days",
every: "every",
EndByLabel: "end by",
EndAfterLabel: "end after",
HttpErrorMessage: "Error reading calendar events:", HttpErrorMessage: "Error reading calendar events:",
CategoryPlaceHolder: "Please select category", CategoryPlaceHolder: "Please select category",
CategoryLabel: "Category", CategoryLabel: "Category",
EnDateValidationMessage: "start date is greater than end date", EnDateValidationMessage: "start date is greater than end date",
SartDateValidationMessage: "start date is greater than end date", SartDateValidationMessage: "start date is greater than end date",
eventSelectDatesLabel: "Show only the events within the following dates", eventSelectDatesLabel: "Show only the events within the following dates",
ConfirmeDeleteMessage: "Confirm delete event ?", ConfirmeDeleteMessage: "Confirm delete event ? If the event is a recurrence event all entries will be deleted ",
DialogConfirmDeleteTitle: " 'Delete Event'", DialogConfirmDeleteTitle: "Delete Event",
SpinnerDeletingLabel: "Deleting...", SpinnerDeletingLabel: "Deleting...",
DialogCloseButtonLabel: "Cancel", DialogCloseButtonLabel: "Cancel",
DialogConfirmDeleteLabel: "Delete", DialogConfirmDeleteLabel: "Delete",
@ -54,7 +74,7 @@ define([], function () {
Oct:'Oct', Oct:'Oct',
Nov:'Nov', Nov:'Nov',
Dez:'Dez', Dez:'Dez',
Dezember: "December", December: "December",
November: " 'November'", November: " 'November'",
October: "October", October: "October",
September: "September", September: "September",

View File

@ -1,4 +1,25 @@
declare interface ICalendarWebPartStrings { declare interface ICalendarWebPartStrings {
WeeksOnLabel: string;
PaternLabel: string;
OcurrencesLabel: string;
dateRangeLabel: string;
weekEndDay: string;
weekDayLabel: string;
lastLabel: string;
fourthLabel: string;
thirdLabel: string;
secondLabel: string;
firstLabel: string;
theLabel: string;
MonthsLabel: string;
ofEveryLabel: string;
AllowedValues1to12Label: string;
noEndDate: string;
everyweekdays: string;
days: string;
every: string;
EndByLabel: string;
EndAfterLabel: string;
HttpErrorMessage: string; HttpErrorMessage: string;
CategoryPlaceHolder: string; CategoryPlaceHolder: string;
CategoryLabel: string; CategoryLabel: string;
@ -52,7 +73,7 @@ declare interface ICalendarWebPartStrings {
Oct:string; Oct:string;
Nov:string; Nov:string;
Dez:string; Dez:string;
Dezember: string; December: string;
November: string; November: string;
October: string; October: string;
September: string; September: string;
@ -91,7 +112,6 @@ declare interface ICalendarWebPartStrings {
previousLabel: string; previousLabel: string;
nextLabel: string; nextLabel: string;
showMore: string; showMore: string;
PropPanelSiteUrlErrorMessage: string;
} }
declare module 'CalendarWebPartStrings' { declare module 'CalendarWebPartStrings' {

View File

@ -1,6 +1,26 @@
define([], function() { define([], function() {
return { return {
PropPanelSiteUrlErrorMessage:'Por favor verifique se site url é valido.', WeeksOnLabel: "week(s) on",
PaternLabel: "Patern",
OcurrencesLabel: "Ocurrences",
dateRangeLabel: "Date Range",
weekEndDay: " 'weekend day'",
weekDayLabel: "weekday",
lastLabel: "last",
fourthLabel: " 'fourth'",
thirdLabel: "third",
secondLabel: " 'Second' ",
firstLabel: "first",
theLabel: "the",
MonthsLabel: "month(s)",
ofEveryLabel: "of every ",
AllowedValues1to12Label: "Allowed values 1 to 12",
noEndDate: "no end date",
everyweekdays: "every weekdays",
days: "days",
every: "every",
EndByLabel: "end by",
EndAfterLabel: "end after",
HttpErrorMessage: "Error reading calendar events:", HttpErrorMessage: "Error reading calendar events:",
CategoryPlaceHolder: "Please select category", CategoryPlaceHolder: "Please select category",
CategoryLabel: "Category", CategoryLabel: "Category",

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.8.2",
"libraryName": "hello-world",
"libraryId": "06aabb02-0b8e-434a-9b14-eae5b3c24411",
"environment": "spo",
"packageManager": "npm",
"isCreatingSolution": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,81 @@
# React Functional Component web part
## Summary
This web part is intended to be easier to understand for new developers building their first SPFx web part. It is a refactoring of the HelloWorld web part that is created by the **@microsoft/generator-sharepoint** Yeoman generator, and introduces React Functional Components.
![Screenshot](Screenshot.png "Screenshot - nothing to see here, move along")
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
This sample was built with version 1.82 of the SharePoint Framework. It has been modified to use version 16.8 of the React framework (by default the version used is React 16.7). React 16.8 supports React Hooks although this is not needed in the sample code because HelloWorld.tsx is a pure (or stateless) functional component.
## Solution
Solution|Author(s)
--------|---------
react-functional-component | Bill Ayers
## Version history
Version|Date|Comments
-------|----|--------
1.0|June 5, 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 /samples/react-functional-component folder
* At the command line run:
* `npm install`
* `gulp serve`
## Features
The purpose of this web part is to make it easier to understand for new developers building their first SPFx web part, when teaching the SharePoint Framework. The web part is a refactoring of the HelloWorld web part that is created by the **@microsoft/generator-sharepoint** Yeoman generator. The resulting rendered web part should look exactly the same, but the complexity of the code has been significantly reduced, and should be much easier to understand for a newcomer to the framework.
It also introduces React Functional Components which offers a simpler way of building React Components using functions instead of classes.
* Simplification
* Functional Component
* Adding State
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-functional-component" />
## HelloWorldWebPart.ts Simplification
A number of simplifications have been made to the HelloWorldWebPart.ts file to make it easier to follow.
The use of an external string collection has been removed. This is only needed in multilingual situations and can be added as and when needed. For a first web part there is really no need to have the student wondering where these strings are defined. For this sample they are simply hard coded into the file to make it clear how the property pane configuration works.
The external interface to define the properties is moved from a separate file and inline into HelloWorldWebPart.ts. This interface is used by the web part and the component on the assumption that all the properties will be passed to the component as props. Adding more properties is simply a matter of adding them to the IHelloWorldProps interface, adding a section to the getPropertyPaneConfiguration return value and adding a default to the manifest file if needed. The property will then be available to the component through its **props** collection.
## Functional Component
The HelloWorld.tsx React Component has been refactored as a pure functional component. This simplifies the code structure and will also gain you additional kudos when talking to computer scientists and functional code enthusiasts. The structure is a simple JavaScript function with the name of the component, a single argument containing the React props, and a simple return of the component rendering. Because it is just a function, there is no need to worry about **this** or **that**, constructors etc.
In addition the React elements returned have been simplified. In particular the "Learn more" button, which was constructed from HTML primitives in the Yeoman-generated sample, has been replaced by an Office-UI-Fabric PrimaryButton component. This also means that it has been possible to greatly simplify the SASS file HelloWorld.module.scss.
## Adding State
You may be wondering how maintaining state, side effects or other complexities can be accomodated with functional components like the one used. This can be achieved using a fairly new feature called [React Hooks](https://reactjs.org/docs/hooks-intro.html) and will be demonstrated using [another sample](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-functional-stateful-component).
## Building and testing
In the react-functional-component directory run **npm install** to resolve all the dependencies. Once this has completed you can run **gulp serve** to test the web part in the local workbench.

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"hello-world-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/helloWorld/HelloWorldWebPart.js",
"manifest": "./src/webparts/helloWorld/HelloWorldWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"HelloWorldWebPartStrings": "lib/webparts/helloWorld/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": "hello-world",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "hello-world-client-side-solution",
"id": "06aabb02-0b8e-434a-9b14-eae5b3c24411",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"skipFeatureDeployment": true,
},
"paths": {
"zippedPackage": "solution/hello-world.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 -->"
}

View File

@ -0,0 +1,14 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.configureWebpack.mergeConfig({
additionalConfiguration: (config) => {
config.externals = config.externals.filter(name => !(["react", "react-dom"].includes(name)))
return config;
}
});
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
{
"name": "hello-world",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"start": "gulp serve",
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.8.2",
"@microsoft/sp-lodash-subset": "1.8.2",
"@microsoft/sp-office-ui-fabric-core": "1.8.2",
"@microsoft/sp-property-pane": "1.8.2",
"@microsoft/sp-webpart-base": "1.8.2",
"@types/es6-promise": "0.0.33",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.143.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"resolutions": {
"@types/react": "16.7.22"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-2.9": "0.7.7",
"@microsoft/rush-stack-compiler-3.3": "^0.2.15",
"@microsoft/sp-build-web": "1.8.2",
"@microsoft/sp-module-interfaces": "1.8.2",
"@microsoft/sp-tslint-rules": "1.8.2",
"@microsoft/sp-webpart-workbench": "1.8.2",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@types/react": "^16.8.19",
"@types/react-dom": "^16.8.4",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"typescript": "^3.5.1"
}
}

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,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "6dd862f6-29a3-41a8-91f0-cf3ffb2daad7",
"alias": "HelloWorldWebPart",
"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": "HelloWorld" },
"description": { "default": "HelloWorld description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "HelloWorld"
}
}]
}

View File

@ -0,0 +1,45 @@
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 HelloWorld from './components/HelloWorld';
export interface IHelloWorldProps {
description: string;
}
export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldProps> {
public render(): void {
ReactDom.render(React.createElement(HelloWorld, this.properties), 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: "Properties"
},
groups: [
{
groupName: "General",
groupFields: [
PropertyPaneTextField('description', { label: "Description Text" })
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,26 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
}

View File

@ -0,0 +1,20 @@
import * as React from 'react';
import styles from './HelloWorld.module.scss';
import { IHelloWorldProps } from '../HelloWorldWebPart';
import { escape } from '@microsoft/sp-lodash-subset';
import * as Fabric from 'office-ui-fabric-react';
export default function HelloWorld(props: IHelloWorldProps) {
return (
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<span className={styles.title}>Welcome to SharePoint!</span>
<p>Customize SharePoint experiences using Web Parts.</p>
<p>{escape(props.description)}</p>
<Fabric.PrimaryButton href="https://aka.ms/spfx">Learn more</Fabric.PrimaryButton>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,2 @@
// dummy locale file to keep the gulp task happy
define([], function() { return {}});

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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

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

View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.8.2",
"libraryName": "roman-numerals",
"libraryId": "f48eaad8-e027-4f27-bfca-0abd5239e65b",
"environment": "spo",
"packageManager": "npm",
"isCreatingSolution": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,92 @@
# React Functional Stateful Component web part
## Summary
This web part demonstrates building a React functional component that includes state, using the recently introduced React Hooks feature. The example web part renders a number to Roman numerals conversion tool.
![Screenshot](Screenshot.png "Screenshot - Roman Numerals web part")
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
This sample was built with version 1.82 of the SharePoint Framework. It has been modified to use version 16.8 of the React framework (by default the version used is React 16.7). React 16.8 supports React Hooks which is required to support state management in a React functional component.
## Solution
Solution|Author(s)
--------|---------
react-functional-stateful-component | Bill Ayers
## Version history
Version|Date|Comments
-------|----|--------
1.0|June 5, 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 /samples/react-functional-stateful-component folder
* At the command line run:
* `npm install`
* `gulp serve`
## Features
The purpose of this web part is to demonstrate building a React functional component that includes state. This is achieved using the recent React Hooks feature. The resulting code is cleaner and easier to follow than using a JavaScript/TypeScript class derived from React.Component. The example web part renders a number to Roman numerals conversion tool, although the functionality is just for the purposes of the demonstration.
This is an extension of the approach used in the [React-Functional-Component](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-functional-component) sample.
* Simplification
* Functional Component
* Adding State
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-functional-stateful-component" />
## RomanNumeralsWebPart.ts Simplification
A number of simplifications have been made to the RomanNumeralsWebPart.ts file to make it easier to follow compared to the Yeoman generator starter project. The use of an external string collection has been removed - they are simply hard coded into the file to make it clear how the property pane configuration works.
The external interface to define the properties is moved from a separate file and inline into RomanNumeralsWebPart.ts. This interface is used by the web part and the component on the assumption that all the properties will be passed to the component as props. The property will then be available to the component through its **props** collection.
## Functional Component
The RomanNumerals.tsx React Component is a React functional component. This simplifies the code structure to a simple JavaScript function with the name of the component, a single argument containing the React props, and a simple return of the component rendering. Because it is just a function, there is no need to worry about **this** or **that**, constructors etc. In this example we are introducing state, so this is not a "pure" or "stateless" functional component. For a more complicated example it might be advantageous to break the component down into several nested components, some of which may well be stateless. Stateless components are very simple and trivially easy to test. But typically we need to manage the state for at least some of our components as described below.
## Adding State
State is managed by means of a fairly new feature called [React Hooks](https://reactjs.org/docs/hooks-intro.html). On line 11 of RomanNumerals.tsx the React.useState function is used to provide state:
```
const [value, setValue] = React.useState(parseInt(props.initialValue));
```
React.useState takes an initial value for the state variable and returns an array of two objects. The first is a variable containing the state value, and the second is a setter function for the value. We could refer to these as state[0] and state[1] or something, but the convention is to use the array destructuring operator to unpack them into local constants. The name of these is not important but a good practice is to use the form *[foo, setFoo]*, etc. Whenever we need to use the current value of the state variable we just refer to it (e.g. *{value}*), and wherever we need to change the value we call *useState(newValue)*. There is no need to use **this** because we are not inside a class, nor do we need to worry about the context of the **this** value, nor create a constructor to initialize state.
In the code we use the number input control to change the value of the state using a local function *onChange*, which simply sets a new value as the user types in characters. We also have a couple of additional buttons that can be used to increment and decrement the value which also demonstrates updating state with inline functions. Everything just works because the React framework will re-render the component whenever we update the state variable using the *setValue* function. If you need more complex state you could pass a more complex object to *useState* but a better approach is often to simply call *useState* once for each variable that makes up the state.
The output rendering uses the value of the state variable and does a conversion using the *romanToString* function:
```
<h3>{props.resultCaption} {romanToString(value)}</h3>
```
The *resultCaption* property is also rendered if defined.
## Building and testing
In the react-functional-component directory run **npm install** to resolve all the dependencies. Once this has completed you can run **gulp serve** to test the web part in the local workbench.

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"roman-numerals-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/romanNumerals/RomanNumeralsWebPart.js",
"manifest": "./src/webparts/romanNumerals/RomanNumeralsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"RomanNumeralsWebPartStrings": "lib/webparts/romanNumerals/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": "roman-numerals",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "roman-numerals-client-side-solution",
"id": "f48eaad8-e027-4f27-bfca-0abd5239e65b",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/roman-numerals.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 -->"
}

View File

@ -0,0 +1,13 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.configureWebpack.mergeConfig({
additionalConfiguration: (config) => {
config.externals = config.externals.filter(name => !(["react", "react-dom"].includes(name)))
return config;
}
});
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "roman-numerals",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.8.2",
"@microsoft/sp-lodash-subset": "1.8.2",
"@microsoft/sp-office-ui-fabric-core": "1.8.2",
"@microsoft/sp-property-pane": "1.8.2",
"@microsoft/sp-webpart-base": "1.8.2",
"@types/es6-promise": "0.0.33",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.143.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"resolutions": {
"@types/react": "16.7.22"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-2.9": "0.7.7",
"@microsoft/rush-stack-compiler-3.3": "^0.2.15",
"@microsoft/sp-build-web": "1.8.2",
"@microsoft/sp-module-interfaces": "1.8.2",
"@microsoft/sp-tslint-rules": "1.8.2",
"@microsoft/sp-webpart-workbench": "1.8.2",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@types/react": "^16.8.19",
"@types/react-dom": "^16.8.4",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"typescript": "^3.5.1"
}
}

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,30 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "0f227452-315b-4061-b627-930468a02acd",
"alias": "RomanNumeralsWebPart",
"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"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "RomanNumerals" },
"description": { "default": "Roman Numeral Converter Web Part" },
"officeFabricIconFontName": "Page",
"properties": {
"title": "Convert Number to Roman Numerals",
"description": "Roman numeral converter - enter a number to see the equivalent in Roman numerals",
"initialValue": "2019",
"showUpdownButtons": false
}
}]
}

View File

@ -0,0 +1,65 @@
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, PropertyPaneToggle } from '@microsoft/sp-property-pane';
import RomanNumerals from './components/RomanNumerals';
export interface IRomanNumeralsProps {
title: string;
description: string;
initialValue: string;
inputCaption: string;
resultCaption: string;
showUpdownButtons: boolean;
}
export default class HelloWorldWebPart extends BaseClientSideWebPart<IRomanNumeralsProps> {
public render(): void {
ReactDom.render(React.createElement(RomanNumerals, this.properties), 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: "Properties"
},
groups: [
{
groupName: "General",
groupFields: [
PropertyPaneTextField('title', { label: "Web part title" }),
PropertyPaneTextField('description', { label: "Description Text" }),
PropertyPaneToggle('showUpdownButtons', { label: "Show increment/decrement buttons" })
]
},
{
groupName: "Initialization",
groupFields: [
PropertyPaneTextField('initialValue', { label: "Initial Value (numeric)" })
]
},
{
groupName: "Captions",
groupFields: [
PropertyPaneTextField('inputCaption', { label: "Caption for input control" }),
PropertyPaneTextField('resultCaption', { label: "Caption for result" })
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,44 @@
export function romanToString(val: number): string {
const roman = [
{ symbol: 'm', number: 1000000 },
{ symbol: 'd', number: 500000 },
{ symbol: 'c', number: 100000 },
{ symbol: 'l', number: 50000 },
{ symbol: 'x', number: 10000 },
{ symbol: 'v', number: 5000 },
{ symbol: 'M', number: 1000 },
{ symbol: 'D', number: 500 },
{ symbol: 'C', number: 100 },
{ symbol: 'L', number: 50 },
{ symbol: 'X', number: 10 },
{ symbol: 'V', number: 5 },
{ symbol: 'I', number: 1 }
];
var negative: boolean = (val < 0.0);
if (negative) val = -val;
var integer: number = Math.floor(val);
if (integer == 0) return "nihil";
var sText: string = "";
if (negative) sText += "minus ";
if (integer > 10000000 || integer > 80 * roman[0].number)
return "Sorry, number is too big for Roman Numerals";
for (var iDigit = 0; iDigit < roman.length; iDigit++) {
var n = Math.floor(integer / roman[iDigit].number);
integer = integer % roman[iDigit].number;
for (var i = 1; i <= n; i++) sText += roman[iDigit].symbol;
if (iDigit % 2 == 0 && iDigit + 2 < roman.length && Math.floor(integer / (roman[iDigit + 2].number * 9)) > 0) {
sText += roman[iDigit + 2].symbol;
sText += roman[iDigit].symbol;
integer -= roman[iDigit + 2].number * 9;
}
if (iDigit + 1 < roman.length && Math.floor(integer / (roman[iDigit + 1].number * 4)) > 0) {
sText += roman[iDigit + 1].symbol;
sText += roman[iDigit].symbol;
integer -= roman[iDigit + 1].number * 4;
}
}
return sText;
}

View File

@ -0,0 +1,26 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
}

View File

@ -0,0 +1,47 @@
import * as React from 'react';
import styles from './RomanNumerals.module.scss';
import { IRomanNumeralsProps } from '../RomanNumeralsWebPart';
import { escape } from '@microsoft/sp-lodash-subset';
import * as Fabric from 'office-ui-fabric-react';
import { romanToString } from '../RomanToString';
export default function RomanNumerals(props: IRomanNumeralsProps) {
// Use React Hooks to manage state - useState returns value and setter as array...
const [value, setValue] = React.useState(parseInt(props.initialValue));
function onChange(event) {
setValue(parseInt(event.target.value));
}
var updownButtons = null;
if (props.showUpdownButtons) updownButtons = (
<div className={styles.column}>
<br />
<Fabric.PrimaryButton onClick={() => setValue(value + 1)}>+</Fabric.PrimaryButton>
&nbsp;
<Fabric.PrimaryButton onClick={() => setValue(value - 1)}>-</Fabric.PrimaryButton>
</div>
);
return (
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<span className={styles.title}>{props.title}</span>
<p>{escape(props.description)}</p>
</div>
<div className={styles.column}>
{props.inputCaption}<br />
<input type="number" min="0" max="9999999" value={value} onChange={onChange} />
</div>
{updownButtons}
<div className={styles.column}>
<br />
<h3>{props.resultCaption} {romanToString(value)}</h3>
<br />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,2 @@
// dummy locale file to keep gulp happy
define([], function() { return {}});

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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

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

View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.8.2",
"libraryName": "react-mobx-multiple-stores",
"libraryId": "94924d67-e7b2-415f-9bd3-3f69b18b37c8",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,52 @@
# Webpart with React and Mobx using multiple stores
## Summary
A sample webpart that uses the [Mobx](https://mobx.js.org/) library with multiple stores to keep track of the applications state.
<img src="assets/demo.gif"/>
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.8.2-green.svg)
## Applies to
* [SharePoint Framework](https://dev.office.com/sharepoint)
* [SharePoint Framework Webpart Samples](https://github.com/SharePoint/sp-dev-fx-webparts)
* [Office 365 developer tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
react-mobx-multiple-stores | Kemal Sinanagic / [@kemicza](http://twitter.com/kemicza) / kemicza@gmail.com
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 24, 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
```sh
$ git clone https://github.com/SharePoint/sp-dev-fx-webparts
$ cd sp-dev-fx-webparts/samples/react-mobx-multiple-stores
$ npm install
$ gulp serve
```
## Features
* Enforces that the state always needs be updated in **actions**, using the <em>always</em> flag for <em>enforceActions</em>.
* Demonstrates the **toJS** method to convert an observable array to a javascript structure. This is used to render the items in a DetailsList.
* Out-of-the-box MobX **decorators** to keep the code clean.
* **Asynchronous** actions
* MobX **computed** values
* **Typescript** version 3.3.4 using <em>@microsoft/rush-stack-compiler-3.3</em> for compatibility with the latest MobX version and typings
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-mobx-multiple-stores" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"mobx-tutorial-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/mobxTutorial/MobxTutorialWebPart.js",
"manifest": "./src/webparts/mobxTutorial/MobxTutorialWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"MobxTutorialWebPartStrings": "lib/webparts/mobxTutorial/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-mobx-multiple-stores",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-mobx-multiple-stores-client-side-solution",
"id": "94924d67-e7b2-415f-9bd3-3f69b18b37c8",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-mobx-multiple-stores.sppkg"
}
}

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