New sample to show my outlook events

This commit is contained in:
chandani@mypersorg.onmicrosoft.com 2021-08-18 13:20:48 +05:30
parent ac646df972
commit 3eaf3034ae
53 changed files with 25940 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,85 @@
# react-my-events
## Summary
This webpart 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 webpart
![Preview](assets/preview.png)
![Preview](assets/preview.gif)
## Used SharePoint Framework Version
![version](https://img.shields.io/npm/v/@microsoft/sp-component-base/latest?color=green)
## 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 ([@Chandani_SPD](https://twitter.com/Chandani_SPD))
## Version history
Version|Date|Comments
-------|----|--------
1.0 | August 18, 2021 | 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
- 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)
<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,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
}
}