Added the Aggregated Calendar Webpart using React framework (#566)

* Initial Release

* Updated Readme

* Update Read Me

* Updated readme with technolgies
This commit is contained in:
Dhaval Shah 2018-07-13 03:11:28 -05:00 committed by Vesa Juvonen
parent 293bbba17a
commit ea3b8efe1e
32 changed files with 18940 additions and 0 deletions

View File

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

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,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "react-aggregated-calendar",
"libraryId": "e904a5f2-6d72-4ac2-96d8-5679cb4c7384",
"environment": "spo"
}
}

View File

@ -0,0 +1,100 @@
# React Aggregated Calendar Webpart
## Summary
This is a sample webpart developed using React Framework to gather the aggregated events from the multiple calendars from multiple sites using Full Calendar from fullcalendar.io
![The web part in action](./assets/react-aggregated-calendar.gif)
The webpart was designed to create an aggregated view of calendar to fetch events from multiple calendars across the sites and site collection.
The webpart will show the event information using the callout functionality of Office UI Fabric
Webpart is developed using below technologies
* React Framework
* Full Calendar(fullcalendar.io)
* jQuery
* Office UI Fabric
## 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
Before you can use this webpart exmaple, you will need atleast one Out of the Box Calendar created.
It is required that the users have view access on the calendar.
## Solution
Solution|Author(s)
--------|---------
react-aggregated-calendar | [Dhaval Shah](https://www.linkedin.com/in/dhavalshah27) ([@beingdhavalshah](https://twitter.com/BeingDhavalShah))
## Version history
Version|Date|Comments
-------|----|--------
1.0 |July 16, 2018 | Initial Release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part displays the events from multiple calendars located in various sites/site collection of sharepoint:
- Aggregated events for Calendar
- Supports Sub-Sites and Site Collection level
- Display of Legend for each Calendar
- Formatted Date time
- Display of Event Details over event click
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-aggregated-calendar" />
## React Aggregated Calendar
### Building the code
```bash
git clone the repo
npm i
npm i -g gulp
gulp
```
This package produces the following:
* lib/* - intermediate-stage commonjs build artifacts
* dist/* - the bundled script, along with other resources
* deploy/* - all resources which should be uploaded to a CDN.
### Build options
gulp clean
gulp test
gulp serve
gulp bundle
gulp package-solution

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

View File

@ -0,0 +1,31 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"react-aggregated-calendar-web-part": {
"components": [{
"entrypoint": "./lib/webparts/reactAggregatedCalendar/ReactAggregatedCalendarWebPart.js",
"manifest": "./src/webparts/reactAggregatedCalendar/ReactAggregatedCalendarWebPart.manifest.json"
}]
}
},
"externals": {
"sp-client-custom-fields": "node_modules/sp-client-custom-fields/dist/sp-client-custom-fields.bundle.js",
"jquery": {
"path": "https://code.jquery.com/jquery-1.11.1.min.js",
"globalName": "jQuery"
},
"moment": "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js",
"fullcalendar": {
"path": "https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js",
"globalName": "jQuery",
"globalDependencies": [
"jquery"
]
}
},
"localizedResources": {
"sp-client-custom-fields/strings": "node_modules/sp-client-custom-fields/lib/loc/{locale}.js",
"ReactAggregatedCalendarWebPartStrings": "lib/webparts/reactAggregatedCalendar/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-aggregated-calendar",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-aggregated-calendar-app",
"id": "e904a5f2-6d72-4ac2-96d8-5679cb4c7384",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-aggregated-calendar.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,38 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
"displayAsWarning": true,
"removeExistingRules": true,
"useDefaultConfigAsBase": false,
"lintConfig": {
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "https://sharepointarena.sharepoint.com/sites/SPFX/CDN/react-aggregated-calendar"
}

View File

@ -0,0 +1,25 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const path = require('path');
const bundleAnalyzer = require('webpack-bundle-analyzer');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
const lastDirName = path.basename(__dirname);
const dropPath = path.join(__dirname, 'temp', 'stats');
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
generateStatsFile: true,
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
logLevel: 'error'
}));
return generatedConfiguration;
}
});
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
{
"name": "react-aggregated-calendar",
"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.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@types/fullcalendar": "^3.8.0",
"@types/jquery": "^3.3.2",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"moment": "^2.22.2",
"react": "15.6.2",
"react-dom": "15.6.2",
"sp-client-custom-fields": "^1.3.7"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"webpack-bundle-analyzer": "^2.13.1"
}
}

View File

@ -0,0 +1,35 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "be293fec-29f2-4db2-869a-604e560980fa",
"alias": "ReactAggregatedCalendarWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": {
"default": "Ayka"
},
"title": {
"default": "React Aggreagated Calendar App"
},
"description": {
"default": "React Aggreagated Calendar App developed by Dhaval Shah"
},
"officeFabricIconFontName": "SearchCalendar",
"properties": {
"header": "My Calendar",
"showWeekends": "Off",
"showLegend": true,
"dateFormat": "MMMM Do YYYY, h:mm a",
"defaultView": "month",
"availableViews": [
"month",
"agendaWeek",
"agendaDay",
"listMonth"
]
}
}]
}

View File

@ -0,0 +1,181 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Log, Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart, IPropertyPaneConfiguration, PropertyPaneTextField, PropertyPaneDropdown, PropertyPaneToggle } from '@microsoft/sp-webpart-base';
import { SPComponentLoader } from '@microsoft/sp-loader';
import * as strings from 'ReactAggregatedCalendarWebPartStrings';
import ReactAggregatedCalendar from './components/ReactAggregatedCalendar';
import { IReactAggregatedCalendarProps } from './components/IReactAggregatedCalendarProps';
import MessageComponent, { IMessageComponentProps } from '../shared/components/MessageComponent';
import { MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { PropertyFieldCustomList, CustomListFieldType } from 'sp-client-custom-fields/lib/PropertyFieldCustomList';
import { SelectedCalendar } from './model/SelectedCalendar';
import { PropertyFieldDropDownSelect } from 'sp-client-custom-fields/lib/PropertyFieldDropDownSelect';
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
/**
* Interface for the Aggregated Calendar Webpart Class Properties
*
* @export
* @interface IReactAggregatedCalendarWebPartProps
*/
export interface IReactAggregatedCalendarWebPartProps {
header: string;
calendarList: SelectedCalendar[];
dateFormat: string;
showLegend: boolean;
}
/**
* Aggregated Calendar Webpart Class
*
* @export
* @class ReactAggregatedCalendarWebPart
* @extends {BaseClientSideWebPart<IReactAggregatedCalendarWebPartProps>}
*/
export default class ReactAggregatedCalendarWebPart extends BaseClientSideWebPart<IReactAggregatedCalendarWebPartProps> {
private availableViews: IDropdownOption[] = require("../shared/availableViews.json");
private timeFormat: IDropdownOption[] = require("../shared/timeFormat.json");
protected onInit(): Promise<void> {
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css');
return super.onInit();
}
/**
* Renders the React Agggregated Calendar Webpart
*
* @memberof ReactAggregatedCalendarWebPart
*/
public render(): void {
Log.verbose("render()", "Inside Render", this.context.serviceScope);
if (this.needsConfiguration()) {
Log.warn("render()", "Webpart not configured", this.context.serviceScope);
this.renderMessage(strings.WebPartNotConfigured, MessageBarType.error, true);
} else {
Log.info("render()", "Webpart configuration not needed", this.context.serviceScope);
const element: React.ReactElement<IReactAggregatedCalendarProps> = React.createElement(
ReactAggregatedCalendar,
{
header: this.properties.header,
selectedCalendarLists: this.properties.calendarList,
context: this.context,
domElement: this.domElement,
dateFormat: this.properties.dateFormat,
showLegend: this.properties.showLegend
}
);
ReactDom.render(element, this.domElement);
}
}
/**
* Gets the data Version of the Webpart
*
* @readonly
* @protected
* @type {Version}
* @memberof ReactAggregatedCalendarWebPart
*/
protected get dataVersion(): Version {
return Version.parse('1.0');
}
/**
* Initializes the SPFx Property Pane of the Aggregated Calendar Webpart
*
* @protected
* @returns {IPropertyPaneConfiguration}
* @memberof ReactAggregatedCalendarWebPart
*/
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('header', {
label: strings.HeaderFieldLabel
}),
PropertyFieldCustomList('calendarList', {
label: strings.SelectCalendarLabel,
value: this.properties.calendarList,
headerText: 'Manage Calendar',
fields: [
{ id: 'CalendarTitle', title: 'Calendar Title', required: true, type: CustomListFieldType.string },
{ id: 'SiteUrl', title: 'Site Url', required: true, type: CustomListFieldType.string },
{
id: 'CalendarListTitle', title: 'Calendar List Title', required: true,
type: CustomListFieldType.string
},
{ id: 'Color', title: 'Color', required: false, type: CustomListFieldType.color }
],
disabled: false,
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
render: this.render.bind(this),
disableReactivePropertyChanges: this.disableReactivePropertyChanges,
properties: this.properties,
context: this.context,
key: 'calendarList'
}),
PropertyPaneDropdown('dateFormat', {
label: strings.SelectDateFormatFieldLabel,
selectedKey: "MMMM Do YYYY, h: mm a",
options: this.timeFormat
}),
PropertyPaneToggle('showLegend', {
label: strings.ShowLegendFieldLabel,
onText: strings.OnTextFieldLabel,
offText: strings.OffTextFieldLabel,
checked: false
})
]
}
]
}
]
};
}
/**
* Check whether Aggregated Calendar needs configuration
* or not
* @private
* @returns {boolean}
* @memberof ReactAggregatedCalendarWebPart
*/
private needsConfiguration(): boolean {
Log.verbose("needsConfiguration()", "calendarList : " + this.properties.calendarList, this.context.serviceScope);
return this.properties.calendarList === null ||
this.properties.calendarList === undefined ||
this.properties.calendarList.length === 0;
}
/**
* Render Message method to render the message component
*
* @private
* @param {string} statusMessage
* @param {MessageBarType} statusMessageType
* @param {boolean} display
* @memberof ReactAggregatedCalendarWebPart
*/
private renderMessage(statusMessage: string, statusMessageType: MessageBarType,
display: boolean): void {
Log.verbose("renderMessage()", "Rendering Message " + statusMessage + " of type " + statusMessageType, this.context.serviceScope);
const messageElement: React.ReactElement<IMessageComponentProps> = React.createElement(
MessageComponent,
{
Message: statusMessage,
Type: statusMessageType,
Display: display
}
);
ReactDom.render(messageElement, this.domElement);
}
}

View File

@ -0,0 +1,17 @@
import { SelectedCalendar } from "../model/SelectedCalendar";
import { WebPartContext } from "@microsoft/sp-webpart-base";
/**
*
*
* @export
* @interface IReactAggregatedCalendarProps
*/
export interface IReactAggregatedCalendarProps {
header: string;
selectedCalendarLists: SelectedCalendar[];
context: WebPartContext;
domElement: HTMLElement;
dateFormat: string;
showLegend: boolean;
}

View File

@ -0,0 +1,146 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
@import '~office-ui-fabric-react/dist/sass/_References.scss';
@import '~office-ui-fabric-react/dist/sass/Fabric.scss';
.closeIconFocus:focus {
outline: none;
}
.calloutInnerEventContent {
margin-top: 10px;
}
.msCalloutclose {
margin: 0;
border: 0;
background: none;
cursor: pointer;
position: absolute;
top: 12px;
right: 12px;
padding: 8px;
width: 32px;
height: 32px;
font-size: $ms-font-size-m;
color: $ms-color-neutralSecondary;
//z-index: ($ms-zIndex-Callout + $ms-zIndex-front);
}
.msCalloutheader {
//z-index: ($ms-zIndex-Callout + $ms-zIndex-middle);
padding: 18px 24px 12px;
background-color: "[theme: inputBackgroundCheckedHovered, default: #0078d7]" !important ;
}
.msCallouttitle {
margin: 0; //font-family: $ms-font-family-semilight;
//font-size: $ms-font-size-xl;
}
.msCalloutinner {
height: 100%;
padding: 0 24px 20px;
}
.msCalloutsubText {
margin: 0; //font-family: $ms-font-family-semilight;
color: $ms-color-neutralPrimary;
font-size: $ms-font-size-s;
}
.reactAggregatedCalendar {
.legend {
border: solid 1px #eee;
font-size: 14px;
text-align: right;
white-space: normal;
}
.outerLegendDiv {
display: inline-block;
padding: 5px 20px 5px 5px;
line-height: 15px;
max-width: 300px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
}
.innerLegendDiv {
border: 1px solid gray;
width: 15px;
height: 15px;
font-size: 1px;
position: relative;
float: left;
margin-right: 5px;
cursor: pointer;
}
.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;
// background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg12;
@include ms-xl12;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,277 @@
import * as React from 'react';
import * as $ from 'jquery';
import * as moment from 'moment';
import 'fullcalendar';
import styles from './ReactAggregatedCalendar.module.scss';
import { IReactAggregatedCalendarProps } from './IReactAggregatedCalendarProps';
import { EnvironmentType, Environment } from '@microsoft/sp-core-library';
import { AggregatedCalendarService } from '../service/AggregatedCalendarService';
import { AggregatedCalendarMockService } from '../service/AggregatedCalendarMockService';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import * as strings from 'ReactAggregatedCalendarWebPartStrings';
import { FullCalendarEvent } from '../model/FullCalendarEvent';
import { DirectionalHint, Callout } from 'office-ui-fabric-react/lib/Callout';
import { Label } from 'office-ui-fabric-react/lib/Label';
/**
* Interface for maintaining ReactAggregatedCalendar webpart state
*
* @export
* @interface IReactAggregatedCalendarState
*/
export interface IReactAggregatedCalendarState {
isCalloutVisible?: boolean;
selectedEvent: FullCalendarEvent;
directionalHint?: DirectionalHint;
isBeakVisible?: boolean;
gapSpace?: number;
beakWidth?: number;
EventElement: HTMLElement;
}
/**
* React Component for ReactAggregatedCalendar Webpart
*
* @export
* @class ReactAggregatedCalendar
* @extends {React.Component<IReactAggregatedCalendarProps, IReactAggregatedCalendarState>}
*/
export default class ReactAggregatedCalendar
extends React.Component<IReactAggregatedCalendarProps, IReactAggregatedCalendarState> {
/**
*Creates an instance of ReactAggregatedCalendar.
* @param {IReactAggregatedCalendarProps} props
* @memberof ReactAggregatedCalendar
*/
public constructor(props: IReactAggregatedCalendarProps) {
super(props);
this.onCalloutDismiss = this.onCalloutDismiss.bind(this);
this.eventClickHandler = this.eventClickHandler.bind(this);
// Initialize the State for ReactAggregatedCalendar
this.state = {
isCalloutVisible: false,
selectedEvent: {
id: 0,
title: '',
color: '',
start: moment(),
end: moment(),
description: '',
location: '',
allDay: false,
category: ''
},
directionalHint: DirectionalHint.bottomCenter,
isBeakVisible: true,
gapSpace: 10,
beakWidth: 20,
EventElement: null
};
}
/**
* componentDidMount
*
* @memberof ReactAggregatedCalendar
*/
public componentDidMount() {
this.renderContents();
}
/**
* componentDidUpdate
*
* @memberof ReactAggregatedCalendar
*/
public componentDidUpdate() {
this.renderContents();
}
/**
* Render method for the ReactAggregatedCalendar React Component
*
* @returns {React.ReactElement<IReactAggregatedCalendarProps>}
* @memberof ReactAggregatedCalendar
*/
public render(): React.ReactElement<IReactAggregatedCalendarProps> {
const { isCalloutVisible } = this.state;
let calendarLegend: JSX.Element[] = ([]);
// Render the Legend for the Calendar Events
calendarLegend = this.props.selectedCalendarLists.map((calendar) => {
let calendarLegendColor = {
'background-color': `${calendar.Color}`
};
return (
<div className={styles.outerLegendDiv} title={calendar.CalendarTitle}>
<div className={styles.innerLegendDiv} style={calendarLegendColor}>
</div>
{calendar.CalendarTitle}
</div>
);
});
// Render the FullCalendar container
return (
<div className={styles.reactAggregatedCalendar}>
<h1>{this.props.header}</h1>
<div >
<div>
<div>
<div id="aggregatedCalendarComp">
</div>
{
this.props.showLegend &&
<div className={styles.legend}>
{calendarLegend}
</div>
}
</div>
</div>
</div>
{isCalloutVisible && (
<Callout
className="ms-CalloutExample"
ariaLabelledBy={'callout-label-1'}
ariaDescribedBy={'callout-description-1'}
role={'alertdialog'}
target={this.state.EventElement}
onDismiss={this.onCalloutDismiss}
gapSpace={this.state.gapSpace}
isBeakVisible={this.state.isBeakVisible}
beakWidth={this.state.beakWidth}
directionalHint={this.state.directionalHint}
setInitialFocus={true}>
<button onClick={this.onCalloutDismiss}
className={css(styles.msCalloutclose, styles.closeIconFocus, 'ms-fontColor-white')} >
<i className="ms-Icon ms-Icon--Clear"></i>
</button>
<div className={css(styles.msCalloutheader, 'ms-fontColor-white')}>
<p className={styles.msCallouttitle}>{this.state.selectedEvent.title}</p>
</div>
<div className={css(styles.msCalloutinner, styles.calloutInnerEventContent)}>
<div className="ms-Callout-content">
<p className={styles.msCalloutsubText} dangerouslySetInnerHTML={this.createMarkup(this.state.selectedEvent.description)} />
<p className={styles.msCalloutsubText}>
<Label>{strings.StartTimeLabel}{this.state.selectedEvent.start.format(this.props.dateFormat)} </Label>
{
this.state.selectedEvent.end !== null &&
<Label>{strings.EndTimeLabel} {this.state.selectedEvent.end.format(this.props.dateFormat)}</Label>
}
{
this.state.selectedEvent.location !== '' &&
<Label>{strings.LocationLabel}{this.state.selectedEvent.location}</Label>
}
{
this.state.selectedEvent.category !== '' &&
<Label>{strings.CategoryLabel}{this.state.selectedEvent.category}</Label>
}
</p>
</div>
</div>
</Callout>
)}
</div>
);
}
/**
* Render the Full Calendar Plugin
*
* @private
* @memberof ReactAggregatedCalendar
*/
private renderContents() {
let containerEl: JQuery = $('#aggregatedCalendarComp');
let eventSourcesArray: any[] = [];
const dataService = (Environment.type === EnvironmentType.Test
|| Environment.type === EnvironmentType.Local) ? new AggregatedCalendarMockService() :
this.props.context.serviceScope.consume(AggregatedCalendarService.serviceKey);
console.log(this.props.selectedCalendarLists);
this.props.selectedCalendarLists.forEach((calendarData) => {
const calendarRestApi: string = calendarData.SiteUrl.trim()
+ '/_api/Web/Lists/GetByTitle(\'' + calendarData.CalendarListTitle.trim() + '\')/items';
eventSourcesArray.push({
events: ((start: moment.Moment, end: moment.Moment, timezone, callback) => {
const startDate = start.format('YYYY-MM-DD');
const endDate = end.format('YYYY-MM-DD');
dataService.getEventsForCalendar(calendarRestApi, calendarData.Color, startDate, endDate)
.then((response: FullCalendarEvent[]) => {
callback(response);
});
})
});
});
containerEl.fullCalendar({
timezone: 'local',
header: {
left: 'prev,next today',
center: 'title'
},
defaultDate: new Date(),
navLinks: true,
editable: true,
eventLimit: true,
eventSources: eventSourcesArray,
eventClick: this.eventClickHandler
});
}
/**
* Click Event handler when the event is clicked on the Calendar
* Display the Callout function to display event details
* @private
* @param {*} eventObj
* @param {*} jsEvent
* @param {*} view
* @memberof ReactAggregatedCalendar
*/
private eventClickHandler(eventObj: any, jsEvent: any, view: any) {
this.setState(() => {
return {
isCalloutVisible: !this.state.isCalloutVisible,
selectedEvent: {
id: eventObj.id,
title: eventObj.title,
color: eventObj.color,
start: moment(eventObj.start),
end: moment(eventObj.end),
description: eventObj.description,
location: eventObj.location,
allDay: eventObj.allDay,
category: eventObj.category
},
EventElement: jsEvent.toElement
};
});
}
/**
* Hide the call out component on close
*
* @private
* @memberof ReactAggregatedCalendar
*/
private onCalloutDismiss() {
this.setState({
isCalloutVisible: false
});
}
/**
* Create markup for rendering HTML on react component
*
* @private
* @returns
* @memberof ReactAggregatedCalendar
*/
private createMarkup(description: string) {
return { __html: description };
}
}

View File

@ -0,0 +1,22 @@
define([], function () {
return {
"PropertyPaneDescription": "React Aggregated Calendar View App",
"BasicGroupName": "Dynamic Properties",
"DescriptionFieldLabel": "Description Field",
"SelectCalendarLabel": "Add Calendars",
"HeaderFieldLabel": "App Header Value",
"WebPartNotConfigured": "Webpart is not configured. Please configure webpart by Editing the page.",
"StartTimeLabel": "Start Time : ",
"EndTimeLabel": "End Time : ",
"LocationLabel": "Location : ",
"CategoryLabel": "Category : ",
"SelectDateFormatFieldLabel": "Select Date & Time Format",
"ShowWeekendsFieldLabel": "Show Weekends",
"OnTextFieldLabel": "On",
"OffTextFieldLabel": "Off",
"DefautlViewFieldLabel": "Default Calendar View",
"ShowLegendFieldLabel": "Show Legend",
"AvailableViewFieldLabel": "Select Available Views",
"HeightFieldLabel":"Enter Calendar Height"
}
});

View File

@ -0,0 +1,25 @@
declare interface IReactAggregatedCalendarWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
SelectDateFormatFieldLabel: string;
ShowWeekendsFieldLabel: string;
WebPartNotConfigured: string;
SelectCalendarLabel: string;
HeaderFieldLabel: string;
StartTimeLabel: string;
EndTimeLabel: string;
LocationLabel: string;
CategoryLabel: string;
OnTextFieldLabel: string;
OffTextFieldLabel: string;
DefautlViewFieldLabel: string;
ShowLegendFieldLabel: string;
AvailableViewFieldLabel: string;
HeightFieldLabel: string;
}
declare module 'ReactAggregatedCalendarWebPartStrings' {
const strings: IReactAggregatedCalendarWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,19 @@
import * as moment from 'moment';
/**
* Interface for FullCalendarEvent
*
* @export
* @interface FullCalendarEvent
*/
export interface FullCalendarEvent {
id: number;
title: string;
start: moment.Moment;
end: moment.Moment;
color: string;
allDay: boolean;
description: string;
location: string;
category: string;
}

View File

@ -0,0 +1,26 @@
/**
* Interface for SPCalendarItemsValue
*
* @export
* @interface SPCalendarItemsValue
*/
export interface SPCalendarItemsValue {
Id: number;
Title: string;
Location?: any;
EventDate: Date;
EndDate: Date;
Description?: any;
fAllDayEvent?: boolean;
Category?: string;
}
/**
* Interface for SPCalendarItems
*
* @export
* @interface SPCalendarItems
*/
export interface SPCalendarItems {
value: SPCalendarItemsValue[];
}

View File

@ -0,0 +1,12 @@
/**
* Interface for SelectedCalendar
*
* @export
* @interface SelectedCalendar
*/
export interface SelectedCalendar {
CalendarTitle: string;
SiteUrl: string;
CalendarListTitle: string;
Color: string;
}

View File

@ -0,0 +1,79 @@
import { IAggregatedCalendarService } from './IAggregatedCalendarService';
import { SelectedCalendar } from '../model/SelectedCalendar';
import { FullCalendarEvent } from '../model/FullCalendarEvent';
import * as moment from 'moment';
/**
* Mock Service for AggregatedCalendarService
*
* @export
* @class AggregatedCalendarMockService
* @implements {IAggregatedCalendarService}
*/
export class AggregatedCalendarMockService implements IAggregatedCalendarService {
/**
* Returns the mock data for the calendar events
*
* @param {string} calendarRestApi
* @param {string} calendarColor
* @param {string} startDate
* @param {string} endDate
* @returns {Promise<FullCalendarEvent[]>}
* @memberof AggregatedCalendarMockService
*/
public getEventsForCalendar(calendarRestApi: string, calendarColor: string, startDate: string,
endDate: string):
Promise<FullCalendarEvent[]> {
return new Promise<FullCalendarEvent[]>((resolve, reject) => {
let calendarLists: FullCalendarEvent[] = [
{
id: 1,
title: "Lunch",
start: moment().add(1, 'days'),
end: moment().add(1, 'days').add(1, "h"),
color: "blue",
allDay: false,
description: "",
location: "18223 Kilmacolm Drive, Richmond, TX 77407",
category: "Get-together"
},
{
id: 2,
title: "Lunch & Learn",
start: moment(),
end: moment().add(1, "h"),
color: "blue",
allDay: false,
description: "<p>Lunch &amp; Learn Session</p>\r\n",
location: "Microsoft Store, 5015 Westheimer Rd Ste A2421, Houston, TX, United States",
category: "Meeting"
},
{
id: 3,
title: "Town Hall",
start: moment("2018-07-08T21:30:00.000Z"),
end: moment("2018-07-08T22:30:00.000Z"),
color: "red",
allDay: false,
description: "",
location: "Deer Park, Texas, United States",
category: ""
},
{
id: 4,
title: "Team Outing",
start: moment("2018-07-12T00:00:00.000Z"),
end: moment("2018-07-12T23:59:00.000Z"),
color: "red",
allDay: false,
description: "",
location: "Seaworld San Antonio, San Antonio, Texas, United States",
category: ""
}
];
resolve(calendarLists);
});
}
}

View File

@ -0,0 +1,82 @@
import { IAggregatedCalendarService } from './IAggregatedCalendarService';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { ServiceKey, ServiceScope, Log } from '@microsoft/sp-core-library';
import { PageContext } from '@microsoft/sp-page-context';
import * as moment from 'moment';
import { SPCalendarItems } from '../model/SPCalendarItems';
import { FullCalendarEvent } from '../model/FullCalendarEvent';
/**
* Aggregated Calendar Service for teh Aggregated Calendar Webpart to get the Calendar Events
*
* @export
* @class AggregatedCalendarService
* @implements {IAggregatedCalendarService}
*/
export class AggregatedCalendarService implements IAggregatedCalendarService {
public static readonly serviceKey: ServiceKey<IAggregatedCalendarService>
= ServiceKey.create<IAggregatedCalendarService>('ayka:IAggregatedCalendarService', AggregatedCalendarService);
private _spHttpClient: SPHttpClient;
private _serviceScope: ServiceScope;
/**
*Creates an instance of AggregatedCalendarService.
* @param {ServiceScope} serviceScope
* @memberof AggregatedCalendarService
*/
constructor(serviceScope: ServiceScope) {
serviceScope.whenFinished(() => {
this._spHttpClient = serviceScope.consume(SPHttpClient.serviceKey);
this._serviceScope = serviceScope;
});
}
/**
* Gets the Events from the SharePoint Calendar between startDate and endDate
*
* @param {string} calendarRestApi
* @param {string} calendarColor
* @param {string} startDate
* @param {string} endDate
* @returns {Promise<any[]>}
* @memberof AggregatedCalendarService
*/
public getEventsForCalendar(calendarRestApi: string, calendarColor: string, startDate: string, endDate: string): Promise<any[]> {
return new Promise<FullCalendarEvent[]>((resolve, reject) => {
let _webRestApi: string = calendarRestApi +
'?$Select=Title,EventDate,EndDate,Location,Description,Category,fAllDayEvent&$filter=((EventDate ge \''
+ startDate + '\' and EventDate le \'' + endDate + '\'))';
Log.info("getEventsForCalendar()", "REST API : " + calendarRestApi, this._serviceScope);
this._spHttpClient.get(_webRestApi, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
response.json().then((spEvents: SPCalendarItems) => {
Log.verbose("getEventsForCalendar()", JSON.stringify(spEvents), this._serviceScope);
let fullCalendarEvents: FullCalendarEvent[] = [];
// Convert the SharePoint Events into compatible Full Calendar Events
spEvents.value.forEach((spEvent) => {
fullCalendarEvents.push({
id: spEvent.Id,
title: spEvent.Title,
start: moment(spEvent.EventDate),
end: moment(spEvent.EndDate),
color: calendarColor,
allDay: spEvent.fAllDayEvent,
description: spEvent.Description || '',
location: spEvent.Location || '',
category: spEvent.Category || ''
});
});
Log.info("getEventsForCalendar()", "Returning Full Calendar Events ", this._serviceScope);
Log.verbose("getEventsForCalendar()", JSON.stringify(fullCalendarEvents), this._serviceScope);
resolve(fullCalendarEvents);
}).catch((error) => {
Log.error("getEventsForCalendar()", new Error("Error Fetching events from Calendar"), this._serviceScope);
reject(error);
});
});
});
}
}

View File

@ -0,0 +1,12 @@
import { FullCalendarEvent } from "../model/FullCalendarEvent";
/**
* Interface Service for the AggregatedCalendarService
*
* @export
* @interface IAggregatedCalendarService
*/
export interface IAggregatedCalendarService {
getEventsForCalendar(calendarRestApi: string, calendarColor: string,
startDate: string, endDate: string): Promise<FullCalendarEvent[]>;
}

View File

@ -0,0 +1,19 @@
[{
"key": "month",
"text": "Month"
},
{
"key": "agendaWeek",
"text": "Weekly"
},
{
"key": "agendaDay",
"text": "Daily"
},
{
"key": "listMonth",
"text": "Monthly List"
}
]

View File

@ -0,0 +1,60 @@
import * as React from 'react';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
/**
* Interface to implement the MessageComponent Webpart
*
* @export
* @interface IMessageComponentProps
*/
export interface IMessageComponentProps {
Message: string;
Type: MessageBarType;
Display: boolean;
}
/**
* React MessageComponent for displaying the messages
*
* @export
* @class MessageComponent
* @extends {React.Component<IMessageComponentProps, any>}
*/
export default class MessageComponent extends React.Component<IMessageComponentProps, any> {
/**
*Creates an instance of MessageComponent.
* @param {IMessageComponentProps} props
* @memberof MessageComponent
*/
public constructor(props: IMessageComponentProps) {
super(props);
}
/**
* Render method of the Message Component
*
* @returns {React.ReactElement<IMessageComponentProps>}
* @memberof MessageComponent
*/
public render(): React.ReactElement<IMessageComponentProps> {
return (
<div className={`ms-Grid-row`}>
<div className={`ms-Grid-col ms-sm12`}>
{
this.props.Display &&
<div>
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={false}
dismissButtonAriaLabel="Close">
{this.props.Message}
</MessageBar>
</div>
}
</div>
</div>
);
}
}

View File

@ -0,0 +1,28 @@
[{
"key": "MMMM Do YYYY, h:mm a",
"text": "MMMM Do YYYY, h:mm a"
},
{
"key": "ddd MMM Do HH:mm YYYY",
"text": "ddd MMM Do HH:mm YYYY"
},
{
"key": "dddd, MMMM Do YYYY, h:mm:ss a",
"text": "dddd, MMMM Do YYYY, h:mm:ss a"
},
{
"key": "dddd, DD MMMM YYYY [at] hh:mm:ss A",
"text": "dddd, DD MMMM YYYY [at] hh:mm:ss A"
},
{
"key": "MM/d/YYYY HH:mm:ss",
"text": "MM/d/YYYYY HH:mm:ss"
},
{
"key": "d/M/YYYY HH:mm:ss",
"text": "d/M/YYYYY HH:mm:ss"
}, {
"key": "dddd, MMMM Do, YYYY, h:mm:ss",
"text": "dddd, MMMM Do, YYYY, h:mm:ss"
}
]

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}