Merge pull request #2020 from chandaniprajapati/new-sample---react-my-events

New sample: react-my-events: Show my outlook events
This commit is contained in:
Hugo Bernier 2021-08-26 01:25:37 -04:00 committed by GitHub
commit 26918d7950
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 26004 additions and 0 deletions

34
samples/react-my-events/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
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
*.scss.d.ts

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.12.1",
"libraryName": "react-my-events",
"libraryId": "40b1d068-bda3-4bc6-8a8f-39eb17bd66ce",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,97 @@
# My Events
## Summary
This web part provides loggedin user's outlook events with some advanced features.
## Features
- It shows outlook events of loggedin users.
- Date range selection of events like this week, this month, next two weeks, this quarter, all upcoming and so on.
- Compact/Flimstarp layout selection
- Show number of events configuartion in compact layout
- Redirect to teams link if there is a teams meeting
- Look and feel is same as OOTB events web part
![Preview](assets/preview.png)
![Preview](assets/preview.gif)
## Compatibility
![SPFx 1.12.1](https://img.shields.io/badge/SPFx-1.12.1-green.svg)
![Node.js LTS v14 | LTS v12 | LTS v10](https://img.shields.io/badge/Node.js-LTS%20v14%20%7C%20LTS%20v12%20%7C%20LTS%20v10-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench")
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
* SharePoint Online tenant
* Approve below permissions from SharePoint Admin.
```
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "Calendars.Read"
},
{
"resource": "Microsoft Graph",
"scope": "Calendars.ReadWrite"
}
]
```
## Solution
Solution|Author(s)
--------|---------
react-my-events | [Chandani Prajapati](https://github.com/chandaniprajapati) ([@Chandani_SPD](https://twitter.com/Chandani_SPD))
## Version history
Version|Date|Comments
-------|----|--------
1.0 | August 18, 2021 | Initial release
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp serve**
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
- [Drive Graph END Point](https://docs.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0)
## 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.**
## Help
We do not support samples, but we this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=bug-report.yml&sample=react-my-events&authors=@Chandani_SPD&title=react-my-events%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=question.yml&sample=react-my-events&authors=@Chandani_SPD&title=react-my-events%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected&template=suggestion.yml&sample=react-my-events&authors=@Chandani_SPD&title=react-my-events%20-%20).
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-my-events" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,52 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-my-events",
"source": "pnp",
"title": "My Events",
"shortDescription": "This web part provides loggedin user's outlook events with some advanced features",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-my-events",
"longDescription": [
"This web part provides loggedin user's outlook events with some advanced features"
],
"created": "2021-08-18",
"modified": "2021-08-18",
"products": [
"SharePoint",
"Office"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.11.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-my-events",
"alt": "react-my-events"
}
],
"authors": [
{
"gitHubAccount": "chandaniprajapati",
"company": "",
"pictureUrl": "https://github.com/chandaniprajapati.png",
"name": "Chandani Prajapati",
"twitter": "Chandani_SPD"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"react-my-events-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/reactMyEvents/ReactMyEventsWebPart.js",
"manifest": "./src/webparts/reactMyEvents/ReactMyEventsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ReactMyEventsWebPartStrings": "lib/webparts/reactMyEvents/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-my-events",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,35 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-my-events-client-side-solution",
"id": "40b1d068-bda3-4bc6-8a8f-39eb17bd66ce",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
},
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.ReadBasic.All"
},
{
"resource": "Microsoft Graph",
"scope": "Calendars.Read"
},
{
"resource": "Microsoft Graph",
"scope": "Calendars.ReadWrite"
}
]
},
"paths": {
"zippedPackage": "solution/react-my-events.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 -->"
}

22
samples/react-my-events/gulpfile.js vendored Normal file
View File

@ -0,0 +1,22 @@
'use strict';
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.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
/* fast-serve */
const { addFastServe } = require("spfx-fast-serve-helpers");
addFastServe(build);
/* end of fast-serve */
build.initialize(require('gulp'));

23671
samples/react-my-events/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "react-my-events",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"serve": "gulp bundle --custom-serve --max_old_space_size=4096 && fast-serve"
},
"dependencies": {
"@fluentui/react": "^8.23.2",
"@microsoft/sp-component-base": "^1.12.1",
"@microsoft/sp-core-library": "1.12.1",
"@microsoft/sp-lodash-subset": "1.12.1",
"@microsoft/sp-office-ui-fabric-core": "1.12.1",
"@microsoft/sp-property-pane": "1.12.1",
"@microsoft/sp-webpart-base": "1.12.1",
"@pnp/spfx-controls-react": "^3.2.1",
"@pnp/spfx-property-controls": "^3.2.0",
"@rehooks/component-size": "^1.0.3",
"moment": "^2.29.1",
"office-ui-fabric-react": "7.156.0",
"react": "16.9.0",
"react-dom": "16.9.0",
"react-slick": "^0.28.1"
},
"devDependencies": {
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.12.1",
"@microsoft/sp-tslint-rules": "1.12.1",
"@microsoft/sp-module-interfaces": "1.12.1",
"@microsoft/sp-webpart-workbench": "1.12.1",
"@microsoft/rush-stack-compiler-3.7": "0.2.3",
"gulp": "~4.0.2",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1",
"spfx-fast-serve-helpers": "~1.12.0"
}
}

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,67 @@
$white: '[theme:white,default:#ffffff]';
$black: '[theme:black,default:#000000]';
.carouselContainer.filmStrip {
margin: 0 -10px;
}
.carouselContainer.filmStrip:global(.slick-slide) {
box-sizing: border-box;
max-height: 300px !important;
margin-left: 50px !important;
}
.indexButtonContainer {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
}
.indexButton {
font-size: 17px;
font-weight: 300;
height: 40px;
padding: 0;
border: 0;
background: 0 0;
cursor: pointer;
color: $white;
width: 40px;
min-width: 20px;
margin-left: 0;
line-height: 40px;
box-sizing: content-box;
background-color: $black;
opacity: .6;
position: absolute;
top: 50%;
transform: translateY(-50%);
transition: all .3s;
}
.carouselContainer .sliderButtons {
opacity: 0;
}
.carouselContainer:hover .sliderButtons {
opacity: 1;
}
.sliderButtons:hover {
color: $white;
}
.indexButton:global(.ms-Button-flexContainer):hover:global(.ms-Icon),
.indexButton:global(.ms-Icon:hover),
.indexButton:hover:global(.ms-Icon) {
color: $white;
}
.leftPositioned {
left: 0;
}
.rightPositioned {
right: 0;
}

View File

@ -0,0 +1,89 @@
import { css } from "@uifabric/utilities/lib/css";
import { IconButton } from "office-ui-fabric-react/lib/Button";
import * as React from "react";
import Slider from "react-slick";
import { ICarouselContainerProps, ICarouselContainerState } from ".";
import styles from "./CarouselContainer.module.scss";
/**
* Carousel container
* Presents the child compoments as a slick slide
*/
export class CarouselContainer extends React.Component<
ICarouselContainerProps,
ICarouselContainerState
> {
// the slick slider used in normal views
private _slider: Slider;
/**
* Renders a slick switch, a slide for each child, and next/previous arrows
*/
public render(): React.ReactElement<ICarouselContainerProps> {
// slick seems to have an issue with having "infinite" mode set to true and having less items than the number of slides per page
// set infinite to true only if there are more than 3 children
var isInfinite: boolean = React.Children.count(this.props.children) > 3;
var settings: any = {
accessibility: true,
arrows: false,
autoplaySpeed: 5000,
dots: true,
infinite: isInfinite,
slidesToShow: 4,
slidesToScroll: 4,
speed: 500,
centerPadding: "50px",
pauseOnHover: true,
variableWidth: false,
useCSS: true,
rows: 1,
respondTo: "slider",
responsive: [
{
breakpoint: 2560,
settings: {
slidesToShow: 3,
slidesToScroll: 3
}
},
{
breakpoint: 801,
settings: {
slidesToShow: 2,
slidesToScroll: 2
}
}
// there is no 1 slide option, as it converts to narrow view
]
};
return (
<div className={css(styles.carouselContainer, styles.filmStrip)}>
<Slider ref={c => (this._slider = c)} {...settings}>
{this.props.children}
</Slider>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons)}
style={{ left: "10px" }}
onClick={() => this._slider.slickPrev()}
>
<IconButton
className={css(styles.indexButton, styles.leftPositioned)}
iconProps={{ iconName: "ChevronLeft" }}
/>
</div>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons)}
style={{ right: "10px" }}
onClick={() => this._slider.slickNext()}
>
<IconButton
className={css(styles.indexButton, styles.rightPositioned)}
iconProps={{ iconName: "ChevronRight" }}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,5 @@
export interface ICarouselContainerProps { }
export interface ICarouselContainerState { }

View File

@ -0,0 +1,2 @@
export * from "./CarouselContainer";
export * from "./CarouselContainer.types";

View File

@ -0,0 +1,49 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
:export {
padding: 20;
minWidth: 210;
maxWidth: 320;
rowsPerPage: 3;
}
.compactLayout {
overflow: hidden;
font-size: 0;
position: relative;
background-color: transparent;
:global(.ms-DocumentCard) {
position: relative;
background-color: $ms-color-white;
height: 100%;
&:global(.ms-DocumentCard--compact), &:global(.ms-DocumentCard--actionable) {
border: none;
:global(.ms-DocumentCardPreview) {
-ms-flex-negative: 0;
flex-shrink: 0;
width: 48px;
}
:global(.ms-DocumentCardTitle) {
font-weight: 400;
}
&:hover {
border: none;
&::after {
border:none;
}
}
}
}
:global(.ms-List-cell) {
vertical-align: top;
display: inline-block;
margin-bottom: 20px;
}
}

View File

@ -0,0 +1,83 @@
import * as React from 'react';
import styles from './CompactLayout.module.scss';
import { FocusZone, FocusZoneDirection, List, css } from "office-ui-fabric-react";
import { ICompactLayoutProps, ICompactLayoutState } from './CompactLayout.types';
import { IRectangle } from 'office-ui-fabric-react/lib/Utilities';
const ROWS_PER_PAGE: number = +styles.rowsPerPage;
const MAX_ROW_HEIGHT: number = +styles.maxWidth;
const PADDING: number = +styles.padding;
const MIN_WIDTH: number = +styles.minWidth;
export default class CompactLayout extends React.Component<ICompactLayoutProps, ICompactLayoutState> {
private _columnCount: number;
private _columnWidth: number;
private _rowHeight: number;
constructor(props: ICompactLayoutProps) {
super(props);
this.state = {
events: [],
currentPage: 1
};
}
public render(): React.ReactElement<ICompactLayoutProps> {
const { items, listProps, ariaLabel } = this.props;
let pagedItems: any[] = items;
return (
<div role="group" aria-label={ariaLabel}>
<FocusZone
direction={FocusZoneDirection.vertical}
isCircularNavigation={false}
aria-label={ariaLabel}
>
<List
className={styles.compactLayout}
items={pagedItems}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
onRenderCell={this._onRenderCell}
{...listProps}
/>
</FocusZone>
</div>
);
}
private _getItemCountForPage = (itemIndex: number, surfaceRect: IRectangle): number => {
if (itemIndex === 0) {
this._columnCount = Math.ceil(surfaceRect.width / (MAX_ROW_HEIGHT));
this._columnWidth = Math.max(MIN_WIDTH, Math.floor(surfaceRect.width / this._columnCount) + Math.floor(PADDING / this._columnCount));
this._rowHeight = this._columnWidth;
}
return this._columnCount * ROWS_PER_PAGE;
}
private _getPageHeight = (): number => {
return this._rowHeight * ROWS_PER_PAGE;
}
private _onRenderCell = (item: any, index: number | undefined): JSX.Element => {
const cellPadding: number = index % this._columnCount !== this._columnCount - 1 && PADDING;
const cellWidth: number = this._columnWidth - PADDING;
return (
<div
style={{
width: `${cellWidth}px`,
marginRight: `${cellPadding}px`
}}
>
{this.props.onRenderGridItem(item, index)}
</div>
);
}
}

View File

@ -0,0 +1,21 @@
import { IListProps } from 'office-ui-fabric-react/lib/List';
export interface ICompactLayoutProps {
ariaLabel?: string;
items: any[];
/**
* In case you want to override the underlying list
*/
listProps?: Partial<IListProps>;
/**
* The method to render each cell item
*/
onRenderGridItem: (item: any, index: number) => JSX.Element;
}
export interface ICompactLayoutState {
events: any[];
currentPage: number;
}

View File

@ -0,0 +1,2 @@
export * from './CompactLayout';
export * from './CompactLayout.types';

View File

@ -0,0 +1,133 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.box {
font-weight: 400;
border: 1px solid;
color: $ms-color-neutralPrimary;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-align: center;
align-items: center;
text-align: center;
}
.box.boxIsSingleDay {
-ms-flex-pack: center;
justify-content: center;
}
.box.boxIsMultipleDays {
-ms-flex-pack: justify;
justify-content: space-between;
}
.date {
-ms-flex-positive: 1;
flex-grow: 1;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-pack: center;
justify-content: center;
}
.boxIsSmall .date {
font-size: 10px;
font-weight: 600;
}
.boxIsMedium .date {
font-size: 12px;
font-weight: 400;
}
.boxIsLarge .date {
font-size: 15px;
font-weight: 400;
}
.day {
font-weight: 600;
}
.dayName {
font-weight: 400;
}
.boxIsSmall .month {
font-size: 12px;
font-weight: 400;
}
.boxIsMedium .month {
font-size: 12px;
font-weight: 600;
}
.boxIsLarge .month {
font-size: 12px;
font-weight: 100;
}
.day {
font-weight: 600;
}
.boxIsSmall .day {
font-size: 21px;
}
.boxIsLarge .day,
.boxIsMedium .day {
font-size: 15px;
}
.box.boxIsSmall {
height: 62px;
width: 62px;
.month {
font-size: 12px;
font-weight: 400;
}
.day {
font-size: 11px;
}
}
.box.boxIsMedium {
height: 80px;
width: 80px;
}
.box.boxIsLarge {
height: 104px;
width: 104px;
}
.boxIsLarge .day,
.boxIsMedium .day {
font-size: 15px;
}
.separator {
margin: 0;
border: 0;
border-bottom: 1px solid;
}
.boxIsSmall .separator {
width: 31px;
}
.boxIsMedium .separator {
width: 40px;
}
.boxIsLarge .separator {
width: 52px;
}

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import { IDateBoxProps, IDateBoxSize } from './IDateBoxProps';
import * as moment from "moment";
import styles from './DateBox.module.scss';
import { css } from 'office-ui-fabric-react';
export default class DateBox extends React.Component<IDateBoxProps, {}>{
public render(): JSX.Element {
let start = moment(this.props.startDate);
let end = moment(this.props.endDate);
let isSameDayEvent: boolean = start.isSame(end, "day");
//Checking whether event is of single day or multiple days.
if (isSameDayEvent) {
return this._renderSingleDay(start);
}
else {
return this._renderMultiDay(start, end);
}
}
/*************************************************************************************************
* Method rendering date box for single day event.
* @param startEvent : Event start date.
*************************************************************************************************/
private _renderSingleDay = (startEvent: moment.Moment): JSX.Element => {
return (
<div className={css(styles.box,
styles.boxIsSingleDay,
(this.props.size === IDateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), this.props.className)}
data-automation-id="singleDayDayContainer">
<div className={styles.month}
data-automation-id="singleDayMonthContainer">{startEvent.format("MMM").toUpperCase()}</div>
<div className={styles.day}
data-automation-id="singleDayDayContainer">{startEvent.format("D")}</div>
<div className={styles.day}
data-automation-id="singleDayDayContainer">{startEvent.format("ddd").toUpperCase()}</div>
</div>);
}
/*************************************************************************************************
* Method rendering date box for multiple day event.
* @param startEvent : Event start date.
* @param endEvent : Enet end date.
*************************************************************************************************/
private _renderMultiDay = (startEvent: moment.Moment, endEvent: moment.Moment): JSX.Element => {
return (
<div
className={css(styles.box,
styles.boxIsSingleDay,
(this.props.size === IDateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), this.props.className
)}
data-automation-id="multipleDayBox">
<div className={styles.date} data-automation-id="multipleDayStartDateContainer">{startEvent.format("MMM D").toUpperCase()}</div>
<hr className={styles.separator} />
<div className={styles.date} data-automation-id="multipleDayEndDateContainer">{endEvent.format("MMM D").toUpperCase()}</div>
</div>);
}
}

View File

@ -0,0 +1,14 @@
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IDateBoxProps {
startDate: any;
endDate: any;
className?: string;
size: IDateBoxSize;
themeVariant?: IReadonlyTheme;
}
export enum IDateBoxSize {
Small,
Medium
}

View File

@ -0,0 +1,214 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
.cardWrapper {
border: 1px solid;
border-color: transparent;
box-sizing: border-box;
outline: transparent;
}
.compactCard {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding: 2px !important;
border: none;
height: 68px;
margin-bottom: 12px;
}
.cardWrapper:focus {
border-color: $ms-color-neutralSecondary;
}
.cardWrapper .dateBox {
border-color: $ms-color-neutralLight;
}
.normalCard .dateBoxContainer {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-align: center;
align-items: center;
}
.normalCard .category {
margin-bottom: 4px;
}
.compactCard .dateBox {
margin: auto 14px auto 0;
min-width: 64px;
background-color: $ms-color-white;
}
[dir="rtl"] .compactCard .dateBox {
margin: auto 0 auto 14px;
}
[dir="ltr"] .compactCard .emptyStatePreviewContainer {
margin-right: 14px;
}
.compactCard .title {
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto,
"Helvetica Neue", sans-serif;
font-size: 16px;
font-weight: 600;
margin: 5px 0 3px;
max-height: 38px;
line-height: 24px;
}
.documentTile {
background-color: transparent;
outline: transparent;
position: relative;
}
.category,
.datetime,
.location {
font-size: 12px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
height: 18px;
margin-top: 12px;
}
.category,
.location {
color: $ms-color-neutralPrimary;
}
.datetime {
color: $ms-color-neutralPrimary;
}
.title {
display: block;
overflow: hidden;
}
// .addToMyCalendar,
// .title {
// font-weight: 600;
// color: $ms-color-neutralPrimary;
// }
.addToMyCalendar {
font-size: 14px;
margin-top: 10px;
}
[dir="ltr"] .addToMyCalendar {
margin-left: -8px;
}
[dir="rtl"] .addToMyCalendar {
margin-right: -8px;
}
.root {
width: 100%;
align-items: center;
-webkit-font-smoothing: antialiased;
border: 1px solid $ms-color-neutralLight;
-webkit-box-sizing: border-box;
box-sizing: border-box;
max-width: 320px;
min-width: 206px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
}
.rootIsCompact {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
max-width: 480px;
height: 68px;
border: 1px solid transparent;
}
.rootIsActionable {
color: inherit;
}
.rootIsActionable:hover {
border-color: $ms-color-neutralLight;
}
.dateBox {
border-color: $ms-color-neutralTertiaryAlt;
}
.normalCard .title {
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto,
"Helvetica Neue", sans-serif;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
height: 32px;
line-height: 21px;
font-weight: 600;
}
.normalCard .detailsContainer {
padding: 0 16px 16px;
height: 160px;
max-width: 320px;
min-width: 206px;
}
.normalCard .iconClass {
font-size: 18px;
height: 18px;
width: 18px;
margin-right: 5px;
}
.normalCard .activityContainer {
display: grid;
grid-template-columns: 90% 10%;
align-items: center;
}
.category,
.location {
color: $ms-color-neutralSecondary;
}
.category,
.datetime,
.location {
font-size: 12px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
height: 22px;
display: flex;
}
.cardActivityRoot {
padding: 6px 0px;
}
.cardActivityDetails {
left: 40px;
}

View File

@ -0,0 +1,150 @@
import * as moment from "moment";
import { DocumentCard, DocumentCardActivity, DocumentCardType, FocusZone, css } from "office-ui-fabric-react";
import * as React from "react";
import { IEventCardProps } from "./IEventCardProps";
import DateBox from "../DateBox/DateBox";
import { IDateBoxSize } from '../DateBox/IDateBoxProps';
import styles from "./EventCard.module.scss";
import { Text } from "@microsoft/sp-core-library";
import * as strings from "ReactMyEventsWebPartStrings";
import { FontIcon } from '@fluentui/react/lib/Icon';
import { mergeStyles } from '@fluentui/react/lib/Styling';
/**
* Shows an event in a document card
*/
export const EventCard = (props: IEventCardProps) => {
const { isCompact, themeVariant, isEditMode, event, layout } = props;
const { start,
end,
allDay,
subject,
organizer,
webLink,
categories,
location,
onlineMeeting,
createdDateTime
} = event;
const eventDate: moment.Moment = moment(start.dateTime);
const endDate: moment.Moment = moment(end.dateTime);
const eventDateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
const endDateString: string = allDay ? endDate.format(strings.AllDayDateFormat) : endDate.format(strings.LocalizedTimeFormat);
const userProfileImg = `/_layouts/15/userphoto.aspx?size=L&username=${organizer.emailAddress.address}`;
const DocumentCardActivityPeople = [{ name: organizer.emailAddress.name, profileImageSrc: userProfileImg, initials: '' }];
const backgroundColor: string = themeVariant && (isCompact ? themeVariant.semanticColors.bodyBackground : themeVariant.palette["primaryBackground"]);
const textColor: string = themeVariant && backgroundColor != themeVariant.semanticColors.bodyText ?
themeVariant.semanticColors.bodyText : themeVariant.palette["primaryText"];
const subTextColor: string = themeVariant && themeVariant.semanticColors.bodySubtext && backgroundColor != themeVariant.semanticColors.bodySubtext ? themeVariant.semanticColors.bodySubtext : textColor;
const created = strings.CreatedLabel + " " + moment(createdDateTime).fromNow();
const teamsMeetingURL = onlineMeeting && onlineMeeting.joinUrl;
const openTeamsLink = (url) => {
window.open(url, '_blank');
};
if (isCompact) {
return (
<div>
<div
className={css(styles.cardWrapper, styles.compactCard, styles.root, styles.rootIsCompact)}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={Text.format(strings.EventCardWrapperArialLabel, subject, eventDateString)}
>
<DocumentCard
type={DocumentCardType.compact}
onClickHref={webLink}
onClickTarget="_blank"
>
<div>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={IDateBoxSize.Small}
themeVariant={themeVariant}
/>
</div>
<div>
<div className={styles.title} style={themeVariant && { color: textColor }}>{subject}</div>
<div className={styles.datetime} style={themeVariant && { color: subTextColor }}>{eventDateString + "-" + endDateString}</div>
</div>
</DocumentCard>
</div>
</div>
);
} else {
return (
<div>
<div
className={css(styles.cardWrapper)}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={Text.format(strings.EventCardWrapperArialLabel, subject, `${eventDateString}`)}
tabIndex={0}
>
<DocumentCard
className={css(styles.root, !isEditMode && styles.rootIsActionable, styles.normalCard)}
type={DocumentCardType.normal}
onClickHref={isEditMode ? null : webLink}
style={themeVariant && { borderColor: themeVariant.semanticColors.bodyDivider }}
>
<FocusZone>
<div className={styles.dateBoxContainer} style={{ height: 120, borderBottom: '1px solid rgb(237, 235, 233)' }}>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={IDateBoxSize.Medium}
themeVariant={themeVariant}
/>
</div>
<div className={styles.detailsContainer}>
<div className={styles.category} style={themeVariant && { color: subTextColor }}>{categories}</div>
<div className={styles.title} style={themeVariant && { color: textColor }}>{subject}</div>
<div className={styles.datetime} style={themeVariant && { color: subTextColor }}>
<FontIcon
aria-label="DateTime"
iconName="DateTime"
className={styles.iconClass} />
{eventDateString + "-" + endDateString}</div>
<div className={styles.location} style={themeVariant && { color: subTextColor }}>
{location.displayName &&
<FontIcon aria-label="Location"
iconName="Location"
className={styles.iconClass} />}
{location.displayName}</div>
<div className={styles.activityContainer}>
<DocumentCardActivity
styles={{
root: styles.cardActivityRoot,
details: styles.cardActivityDetails
}}
activity={created}
people={DocumentCardActivityPeople}
/>
{teamsMeetingURL &&
<FontIcon
aria-label="TeamsLogo16"
iconName="TeamsLogo16"
onClick={() => openTeamsLink(teamsMeetingURL)}
className={styles.iconClass} />
}
</div>
</div>
</FocusZone>
</DocumentCard>
</div>
</div>
);
}
};

View File

@ -0,0 +1,9 @@
import { ICalendarEvent } from "../../models/ICalendarEvent";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IEventCardProps {
layout?: any;
event: ICalendarEvent;
isEditMode?: boolean;
isCompact?: any;
themeVariant?: IReadonlyTheme;
}

View File

@ -0,0 +1,117 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
:export {
centerPadding: 50px;
}
.filmstripLayout {
position: relative;
&.filmStrip {
margin-bottom: 27px;
margin-left: -10px;
margin-right: -10px;
:global(.slick-slide) {
box-sizing: border-box;
padding: 0 10px;
}
}
.sliderButtons {
opacity: 0;
}
&:hover .sliderButtons {
opacity: 1;
&:hover {
color: $ms-color-white;
}
}
}
.indexButtonContainer {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
}
.indexButton {
font-size: 28px;
font-weight: 400;
height: 40px;
padding: 0;
border: 0;
background: 0 0;
cursor: pointer;
color: $ms-color-white;
width: 40px;
min-width: 20px;
margin-left: 0;
line-height: 40px;
box-sizing: content-box;
background-color: $ms-color-black;
opacity: 0.6;
position: absolute;
top: 50%;
transform: translateY(-50%);
transition: all 0.3s;
&:hover {
color: $ms-color-white;
background-color: $ms-color-black;
opacity: 0.6;
}
&:active {
outline: -webkit-focus-ring-color auto 1px;
}
}
.carouselDotsContainer {
.carouselDot {
display: inline-block;
background-color: $ms-color-black;
height: 4px;
width: 4px;
opacity: 0.5;
border: 2px solid $ms-color-black;
border-radius: 4px;
cursor: pointer;
opacity: 0.25;
outline: 0;
&:hover {
opacity: 1;
}
}
}
:global(.slick-active) {
.carouselDotsContainer {
.carouselDot {
background-color: "[theme:themeDark, default: #005a9e]";
opacity: 0.75;
border-color: "[theme:themeDark, default: #005a9e]";
&:hover {
opacity: 1;
}
}
}
}
.indexButton:global(.ms-Button-flexContainer):hover:global(.ms-Icon),
.indexButton:global(.ms-Icon:hover),
.indexButton:hover:global(.ms-Icon) {
color: $ms-color-white;
}
.leftPositioned {
left: 0;
}
.rightPositioned {
right: 0;
}

View File

@ -0,0 +1,115 @@
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { css } from '@uifabric/utilities/lib/css';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import * as React from 'react';
import Slider from 'react-slick';
import { SPComponentLoader } from '@microsoft/sp-loader';
import styles from "./FilmstripLayout.module.scss";
import { useRef } from 'react';
import useComponentSize, { ComponentSize } from '@rehooks/component-size';
function useBreakpoints(currentWidth: number, breakpoints: number[]) {
return breakpoints.map(breakpoint => currentWidth < breakpoint);
}
/**
* Filmstrip layout
* Presents the child compoments as a slick slide
*/
export const FilmstripLayout = (props: { children?: any; clientWidth?: number; themeVariant?: IReadonlyTheme, ariaLabel?: string; }) => {
let ref: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
let size: ComponentSize = useComponentSize(ref);
let { width } = size;
// // the slick slider used in normal views
let _slider: React.MutableRefObject<Slider> = useRef<Slider>(null);
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick.min.css');
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick-theme.min.css');
const [isSmall, isMedium] = useBreakpoints(props.clientWidth, [696, 928]);
// slick seems to have an issue with having "infinite" mode set to true and having less items than the number of slides per page
// set infinite to true only if there are more than 3 children
let numSlides: number = 3;
if (width) {
if (width > 927) {
numSlides = 4;
} else if (width <= 695) {
numSlides = 2;
}
}
var isInfinite: boolean = React.Children.count(props.children) > numSlides;
var settings: any = {
accessibility: true,
arrows: false,
autoplaySpeed: 5000,
dots: true,
customPaging: (i: number) => {
return (
<a>
<div role="button" className={styles.carouselDotsContainer} aria-label={`Carousel Dot ${i}`} data-is-focusable={true} tabIndex={0}>
<span className={styles.carouselDot} tabIndex={-1}></span>
</div>
</a>
);
},
infinite: isInfinite,
slidesToShow: numSlides,
slidesToScroll: numSlides,
speed: 500,
centerPadding: styles.centerPadding,
pauseOnHover: true,
variableWidth: false,
useCSS: true,
rows: 1,
respondTo: "slider",
};
return (
<div>
{props.themeVariant && <style>{`
.${styles.carouselDot} {
background-color: ${props.themeVariant.palette.black}!important;
border-color: ${props.themeVariant.palette.black}!important;
}
.slick-active .${styles.carouselDot} {
background-color: ${props.themeVariant.palette.themeDark}!important;
border-color: ${props.themeVariant.palette.themeDark}!important;
}
.${styles.filmstripLayout} .ms-DocumentCard--actionable:hover {
border-color: ${props.themeVariant.semanticColors.variantBorderHovered}!important;
}
`}
</style>
}
<div className={css(styles.filmstripLayout, styles.filmStrip)} aria-label={props.ariaLabel} ref={ref}>
<Slider ref={_slider} {...settings}>
{props.children}
</Slider>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons)}
style={{ left: "10px" }}
onClick={() => _slider.current.slickPrev()}
>
<IconButton
className={css(styles.indexButton, styles.leftPositioned)}
iconProps={{ iconName: "ChevronLeft", styles: { root: { fontSize: '28px', fontWeight: '400' } } }}
/>
</div>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons)}
style={{ right: "10px" }}
onClick={() => _slider.current.slickNext()}
>
<IconButton
className={css(styles.indexButton, styles.rightPositioned)}
iconProps={{ iconName: "ChevronRight", styles: { root: { fontSize: '28px', fontWeight: '400' } } }}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./FilmstripLayout";

View File

@ -0,0 +1,9 @@
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IPaginationProps {
currentPage: number;
totalItems: number;
itemsCountPerPage: number;
showPageNum: boolean;
onPageUpdate: (pageNumber: number) => void;
}

View File

@ -0,0 +1,47 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
.Pagination {
width: 100%;
min-width: 240px;
text-align: center;
margin: 0;
padding: 2px 0;
border: 1px solid transparent;
position: relative;
.next,
.prev {
margin: 0;
display: inline-block;
border: none;
}
.nogo {
color: $ms-color-neutralTertiary!important;
}
button {
font-size: 14px;
font-weight: 400;
padding: 8px 4px;
margin: 0 8px;
cursor: pointer;
cursor: hand;
position: relative;
height: 32px !important;
display: none;
outline: 0;
&:hover, &:active {
color: "[theme:buttonTextCheckedHovered, default: #000]"
}
}
&.noPageNum {
text-align: left;
.next {
float: right;
}
}
}

View File

@ -0,0 +1,75 @@
import { ActionButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
import { Icon } from "office-ui-fabric-react/lib/Icon";
import { css } from "office-ui-fabric-react/lib/Utilities";
import * as React from "react";
import { IPaginationProps } from ".";
import styles from "./Pagination.module.scss";
import * as strings from "ReactMyEventsWebPartStrings";
import { useCallback } from 'react';
/**
* A custom pagination control designed to look & feel like Office UI Fabric
*/
export const Pagination = (props: IPaginationProps) => {
const { currentPage, totalItems, itemsCountPerPage } = props;
// calculate the page situation
const numberOfPages: number = Math.round(totalItems / itemsCountPerPage);
// we disable the previous button if we're on page 1
const prevDisabled: boolean = currentPage <= 1;
// we disable the next button if we're on the last page
const nextDisabled: boolean = currentPage >= numberOfPages;
/**
* Increments the page number unless we're on the last page
*/
const _nextPage = useCallback((): void => {
if (props.currentPage < numberOfPages) {
props.onPageUpdate(props.currentPage + 1);
}
}, [props, numberOfPages]);
/**
* Decrements the page number unless we're on the first page
*/
const _prevPage = useCallback((): void => {
if (props.currentPage > 1) {
props.onPageUpdate(props.currentPage - 1);
}
}, [props]);
return (
<div className={css(styles.Pagination, props.showPageNum ? null : styles.noPageNum)}>
<ActionButton className={css(styles.prev, prevDisabled && styles.nogo)}
onRenderIcon={(_props: IButtonProps) => {
// we use the render custom icon method to render the icon consistently with the right icon
return (
<Icon iconName="ChevronLeft" />
);
}}
disabled={prevDisabled}
onClick={_prevPage}
ariaLabel="Prev"
>
Prev </ActionButton>
{/* NOT IMPLEMENTED: Page numbers aren't shown here, but we'll need them if we want this control to be reusable */}
<ActionButton className={css(styles.next, nextDisabled && styles.nogo)}
data-automation-id="nextPage"
disabled={nextDisabled}
onRenderMenuIcon={(_props: IButtonProps) => {
// we use the render custom menu icon method to render the icon to the right of the text
return (
<Icon iconName="ChevronRight" />
);
}}
onClick={_nextPage}
ariaLabel="Next"
>
Next
</ActionButton>
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from "./Pagination";
export * from "./IPagingProps";

View File

@ -0,0 +1,29 @@
import * as React from 'react';
import { Persona, PersonaSize } from 'office-ui-fabric-react/lib/Persona';
interface IProfilePicProps {
loginName: string;
displayName: string;
getUserProfileUrl: () => Promise<string>;
}
export function RenderProfilePicture(props: IProfilePicProps) {
const [profileUrl, setProfileUrl] = React.useState<string>();
let { displayName, getUserProfileUrl } = props;
React.useEffect(() => {
getUserProfileUrl().then(url => {
setProfileUrl(url);
});
}, [props]);
return (
<Persona
imageUrl={profileUrl}
text={displayName}
size={PersonaSize.size32}
imageAlt={displayName}
styles={{ primaryText: { fontSize: '12px' } }}
/>);
}

View File

@ -0,0 +1,32 @@
export interface ICalendarEvent {
subject: string;
start: {
dateTime: string | undefined,
};
end: {
dateTime: string | undefined,
};
createdDateTime: string | undefined;
webLink: string | undefined;
allDay: boolean;
categories: string | undefined;
organizer: {
emailAddress: {
name: string | undefined,
address: string | undefined
}
};
onlineMeeting: {
joinUrl: string | undefined;
};
description: string | undefined;
location: {
address: {}
coordinates: {}
displayName: string | undefined;
locationType: string | undefined;
uniqueIdType: string | undefined;
};
eventLocation: string | undefined;
}

View File

@ -0,0 +1,7 @@
export enum DateRange {
AllUpcoming,
ThisWeek,
NextTwoWeeks,
Month,
Quarter
}

View File

@ -0,0 +1,4 @@
export enum Layouts{
filmstrip = 0,
compact = 1
}

View File

@ -0,0 +1,33 @@
import { MSGraphClient } from "@microsoft/sp-http";
export class CalendarService {
public async getEvents(ctx: any, startDate: any, endDate: any) {
try {
let queryString = "?top=365&startdatetime=" + startDate + "&enddatetime=" + endDate;
return new Promise<any>(async (resolve, reject) => {
ctx.msGraphClientFactory
.getClient()
.then((client: MSGraphClient) => {
client
.api("/me/calendarview" + queryString)
.version("v1.0")
.get((error: any, response: any) => {
if (error) {
reject(error);
}
if (response) {
resolve(response.value);
}
});
});
});
}
catch (err) {
Promise.reject(err);
}
}
}

View File

@ -0,0 +1,31 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "12d57dab-78b3-48d3-a405-a15fbcbcaea3",
"alias": "ReactMyEventsWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportsThemeVariants": true,
"supportedHosts": [
"SharePointWebPart"
],
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": {
"default": "Other"
},
"title": {
"default": "react-my-events"
},
"description": {
"default": "react-my-events description"
},
"officeFabricIconFontName": "EventInfo",
"properties": {
"webpartTitle": "My Outlook Events"
}
}
]
}

View File

@ -0,0 +1,152 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { DisplayMode, Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
PropertyPaneChoiceGroup
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'ReactMyEventsWebPartStrings';
import ReactMyEvents from './components/ReactMyEvents';
import { IReactMyEventsProps } from './components/IReactMyEventsProps';
import { PropertyFieldNumber } from '@pnp/spfx-property-controls/lib/PropertyFieldNumber';
import { MSGraphClient } from '@microsoft/sp-http';
import { Layouts } from '../../shared/models/ILayouts';
import { DateRange } from '../../shared/models/IDateRange';
import {
ThemeProvider,
ThemeChangedEventArgs,
IReadonlyTheme,
ISemanticColors
} from '@microsoft/sp-component-base';
export interface IReactMyEventsWebPartProps {
webpartTitle: string;
maxEvents: number;
dateRange: DateRange;
layout: number;
displayMode: DisplayMode;
}
export default class ReactMyEventsWebPart extends BaseClientSideWebPart<IReactMyEventsWebPartProps> {
private _themeProvider: ThemeProvider;
private _themeVariant: IReadonlyTheme | undefined;
private graphClient: MSGraphClient;
protected onInit(): Promise<void> {
// Consume the new ThemeProvider service
this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
// If it exists, get the theme variant
this._themeVariant = this._themeProvider.tryGetTheme();
// Register a handler to be notified if the theme variant changes
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
return super.onInit();
}
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
this._themeVariant = args.theme;
this.render();
}
public render(): void {
const { clientWidth } = this.domElement;
const element: React.ReactElement<IReactMyEventsProps> = React.createElement(
ReactMyEvents,
{
webpartTitle: this.properties.webpartTitle,
dateRange: this.properties.dateRange,
graphClient: this.graphClient,
displayMode: this.displayMode,
layout: this.properties.layout,
context: this.context,
clientWidth: clientWidth,
themeVariant: this._themeVariant,
maxEvents: this.properties.maxEvents,
updateProperty: (value: string) => {
this.properties.webpartTitle = value;
},
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
let maxEventsForCompact: any = [];
if (this.properties.layout === Layouts.compact) {
maxEventsForCompact = PropertyFieldNumber('maxEvents', {
label: strings.NoOfEventsFieldLabel,
key: "maxEventsFieldId",
value: this.properties.maxEvents,
minValue: 0
});
}
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('webpartTitle', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneDropdown("dateRange", {
label: strings.DateRangeFieldLabel,
options: [
{ key: DateRange.AllUpcoming, text: strings.DateRangeOptionUpcoming },
{ key: DateRange.ThisWeek, text: strings.DateRangeOptionWeek },
{ key: DateRange.NextTwoWeeks, text: strings.DateRangeOptionTwoWeeks },
{ key: DateRange.Month, text: strings.DateRangeOptionMonth },
{ key: DateRange.Quarter, text: strings.DateRangeOptionQuarter }
],
selectedKey: this.properties.dateRange,
}),
PropertyPaneChoiceGroup('layout', {
label: "Layouts",
options: [
{
iconProps: { officeFabricIconFontName: 'DockLeftMirrored' },
key: Layouts.compact,
text: "Compact"
},
{
iconProps: { officeFabricIconFontName: 'Tiles' },
key: Layouts.filmstrip,
text: "FilmStrip",
}
],
}),
maxEventsForCompact
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,18 @@
import { MSGraphClient } from "@microsoft/sp-http";
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { DisplayMode } from '@microsoft/sp-core-library';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IReactMyEventsProps {
webpartTitle: string;
layout: number;
context: WebPartContext;
dateRange: any;
displayMode: DisplayMode;
clientWidth: number;
graphClient: MSGraphClient;
themeVariant: IReadonlyTheme;
maxEvents: number;
updateProperty: (value: string) => void;
}

View File

@ -0,0 +1,9 @@
import { ICalendarEvent } from '../../../shared/models/ICalendarEvent';
export interface IReactMyEventsState {
events: ICalendarEvent[];
currentPage: number;
loading: boolean;
errorMessage: string;
noEventsFoundMessage: string;
}

View File

@ -0,0 +1,34 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
.reactMyEvents {
color: inherit;
}
.webPartChrome {
color: inherit;
}
.headerSmMargin {
margin-bottom: 11px;
}
.compact {
color: inherit;
}
.webPartHeader {
display: -ms-flexbox;
display: flex;
-ms-flex-align: baseline;
align-items: baseline;
}
.content {
-ms-flex-order: 2;
order: 2;
width: 100%;
}
.spinner {
padding-top: 20px;
}

View File

@ -0,0 +1,259 @@
import * as React from 'react';
import styles from './ReactMyEvents.module.scss';
import * as moment from 'moment';
import * as strings from 'ReactMyEventsWebPartStrings';
import { IReactMyEventsProps } from './IReactMyEventsProps';
import { FilmstripLayout } from "../../../shared/components/FilmstripLayout/index";
import CompactLayout from "../../../shared/components/CompactLayout/CompactLayout";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { IReactMyEventsState } from './IReactMyEventsState';
import { EventCard } from "../../../shared/components/EventCard/EventCard";
import { Pagination } from "../../../shared/components/Pagination";
import { CalendarService } from '../../../shared/services/CalendarService';
import { DisplayMode } from '@microsoft/sp-core-library';
import { ICalendarEvent } from '../../../shared/models/ICalendarEvent';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { DateRange } from '../../../shared/models/IDateRange';
import { css, Spinner } from "office-ui-fabric-react";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { MessageBar, MessageBarType } from '@fluentui/react';
import { Layouts } from '../../../shared/models/ILayouts';
export default class ReactMyEvents extends React.Component<IReactMyEventsProps, IReactMyEventsState> {
//get todday's date
//private today = undefined;//= moment().format('YYYY-MM-DD');
private today = moment().startOf('week').day('Monday').format('YYYY-MM-DD');
private endDate = undefined;
private _services: CalendarService = undefined;
constructor(props: IReactMyEventsProps) {
super(props);
this._services = new CalendarService();
this.getStartEndDate = this.getStartEndDate.bind(this);
this.getEvents = this.getEvents.bind(this);
this.state = {
events: [],
currentPage: 1,
errorMessage: undefined,
loading: false,
noEventsFoundMessage: undefined
};
}
/** Function to find start and end date based on selected range */
public getStartEndDate() {
switch (this.props.dateRange) {
case DateRange.AllUpcoming:
//this.endDate = moment().add(1, "years").format('YYYY-MM-DD');
this.endDate = moment().endOf('week').day('Friday').add(1, 'years').format('YYYY-MM-DD');
break;
case DateRange.ThisWeek:
this.endDate = moment().endOf('week').day('Friday').format('YYYY-MM-DD');
//this.endDate = moment().add(1, "weeks").format('YYYY-MM-DD');
break;
case DateRange.NextTwoWeeks:
//this.endDate = moment().add(3, "weeks").format('YYYY-MM-DD');
this.endDate = moment().endOf('week').day('Friday').add(2, 'week').format('YYYY-MM-DD');
break;
case DateRange.Month:
//this.endDate = moment().add(1, "months").format('YYYY-MM-DD');
this.endDate = moment().endOf('week').day('Friday').add(1, "months").format('YYYY-MM-DD');
break;
case DateRange.Quarter:
//this.endDate = moment().add(1, "quarters").format('YYYY-MM-DD');
this.endDate = moment().endOf('week').day('Friday').add(1, "quarters").format('YYYY-MM-DD');
break;
}
}
public componentDidMount() {
this.getEvents();
}
public componentDidUpdate(prevProps) {
if (this.props.dateRange != prevProps.dateRange) {
this.getEvents();
}
}
public async getEvents() {
this.getStartEndDate();
this.setState({ loading: true });
await this._services
.getEvents(this.props.context, this.today, this.endDate)
.then((data: any) => {
if (data.length === 0) {
this.setState({
loading: false,
noEventsFoundMessage: strings.NoEventsFoundFieldLabel
});
}
else {
this.setState({
loading: false,
events: data
});
}
}).catch(error => {
this.setState({
loading: false,
errorMessage: "Error while retrieving events: " + error.message
});
});
}
private _renderFilmstripList(): JSX.Element {
const {
events } = this.state;
const isEditMode: boolean = this.props.displayMode === DisplayMode.Edit;
return (<div>
<div>
<div role="application">
<FilmstripLayout
ariaLabel={strings.FilmStripAriaLabel}
clientWidth={this.props.clientWidth}
themeVariant={this.props.themeVariant}>
{events.map((event: ICalendarEvent, index: number) => {
return (<EventCard
key={`eventCard${index}`}
isEditMode={isEditMode}
event={event}
isCompact={false}
themeVariant={this.props.themeVariant} />
);
})}
</FilmstripLayout>
</div>
</div>
</div>);
}
private _renderCompactList(): JSX.Element {
const {
events,
currentPage
} = this.state;
const { maxEvents } = this.props;
let pagedItems: any[] = this.state.events;
const totalItems: number = pagedItems.length;
let showPages: boolean = false;
let pagedEvents: ICalendarEvent[] = events;
let usePaging: boolean = false;
if (+maxEvents > 0 && events.length > +maxEvents) {
// calculate the page size
const pageStartAt: number = +maxEvents * (currentPage - 1);
const pageEndAt: number = (+maxEvents * currentPage);
pagedEvents = events.slice(pageStartAt, pageEndAt);
usePaging = true;
}
return (
<div className={styles.compact}>
<CompactLayout
items={pagedEvents}
onRenderGridItem={(item: any, index: number) => this._onRenderCompactItem(item, index)} />
{usePaging &&
<Pagination
showPageNum={true}
currentPage={currentPage}
itemsCountPerPage={maxEvents}
totalItems={events.length}
onPageUpdate={this._onPageUpdate}
/>
}
</div>
);
}
private _onPageUpdate = (pageNumber: number): void => {
this.setState({
currentPage: pageNumber
});
}
private _onRenderCompactItem = (item: any, _index: number): JSX.Element => {
return <div
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={item.subject}
>
<EventCard
key={`eventCard${_index}`}
event={item}
isCompact={Layouts.compact}
layout={this.props.layout}
themeVariant={this.props.themeVariant} />
</div>;
}
private _onConfigure = () => {
this.props.context.propertyPane.open();
}
private _renderContent(): JSX.Element {
const isCompact = this.props.layout;
const { errorMessage, events, loading } = this.state;
if (loading) {
return (<div className={styles.spinner}><Spinner label="Loading events" /></div>);
}
if (events && events.length) {
if (isCompact === Layouts.compact) {
return this._renderCompactList();
} else {
return this._renderFilmstripList();
}
}
else if (errorMessage) {
return (<MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>);
}
else if (events.length === 0) {
// we're done loading, no errors, but have no events
return (<MessageBar
messageBarType={MessageBarType.error}>{this.state.noEventsFoundMessage}</MessageBar>);
}
}
public render(): React.ReactElement<IReactMyEventsProps> {
const { semanticColors }: IReadonlyTheme = this.props.themeVariant;
return (
<div className={css(styles.reactMyEvents, styles.webPartChrome)} style={{ backgroundColor: semanticColors.bodyBackground }}>
{this.props.dateRange != undefined ?
<><div className={css(styles.webPartHeader, styles.headerSmMargin)}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.webpartTitle}
updateProperty={this.props.updateProperty} />
</div><div className={styles.content}>
{this._renderContent()}
</div></>
:
<Placeholder
iconName="Calendar"
iconText={strings.PlaceholderTitle}
description={strings.PlaceholderDescription}
buttonLabel={strings.ConfigureButton}
onConfigure={this._onConfigure} />
}
</div>
);
}
}

View File

@ -0,0 +1,25 @@
define([], function () {
return {
AllDayDateFormat: "dddd, MMMM Do YYYY",
BasicGroupName: "Group Name",
ConfigureButton: "Configure",
DateRangeFieldLabel: "Date range",
DateRangeOptionUpcoming: "All Upcoming",
DateRangeOptionWeek: "This week",
DateRangeOptionTwoWeeks: "Next two weeks",
DateRangeOptionMonth: "This month",
DateRangeOptionQuarter: "This quarter",
DescriptionFieldLabel: "Description Field",
EventCardWrapperArialLabel: "Event {0}. Start on {1}.",
FilmStripAriaLabel: "Events list. Use left and right arrow keys to move between events. Press enter to go to the selected event.",
FocusZoneAriaLabelReadMode: "Events list. Use up and down arrow keys to move between events. Press enter to obtain details on a selected event.",
FocusZoneAriaLabelEditMode: "Events list. Use up and down arrow keys to move between events.",
LocalizedTimeFormat: "LT", //"llll",
NoOfEventsFieldLabel: "Number of events to show",
NoEventsFoundFieldLabel: "No events found",
PlaceholderTitle: "Configure event feed",
PlaceholderDescription: "To display a summary of my calendar events, you need to select a date range.",
PropertyPaneDescription: "Description",
CreatedLabel: "Created"
}
});

View File

@ -0,0 +1,28 @@
declare interface IReactMyEventsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
NoOfEventsFieldLabel: string;
NoEventsFoundFieldLabel: string;
DateRangeFieldLabel: string;
DateRangeOptionUpcoming: string;
DateRangeOptionWeek: string;
DateRangeOptionTwoWeeks: string;
DateRangeOptionMonth: string;
DateRangeOptionQuarter: string;
AllDayDateFormat: string;
LocalizedTimeFormat: string;
EventCardWrapperArialLabel:string
FilmStripAriaLabel: string;
FocusZoneAriaLabelReadMode:string;
FocusZoneAriaLabelEditMode: string;
PlaceholderTitle: string;
PlaceholderDescription: string;
ConfigureButton: string;
CreatedLabel: string;
}
declare module 'ReactMyEventsWebPartStrings' {
const strings: IReactMyEventsWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/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": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "./node_modules/@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
}
}