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:
commit
26918d7950
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "./release/assets/"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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'));
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
export interface ICarouselContainerProps { }
|
||||
|
||||
export interface ICarouselContainerState { }
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./CarouselContainer";
|
||||
export * from "./CarouselContainer.types";
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './CompactLayout';
|
||||
export * from './CompactLayout.types';
|
|
@ -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;
|
||||
}
|
|
@ -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>);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./FilmstripLayout";
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./Pagination";
|
||||
export * from "./IPagingProps";
|
|
@ -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' } }}
|
||||
/>);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export enum DateRange {
|
||||
AllUpcoming,
|
||||
ThisWeek,
|
||||
NextTwoWeeks,
|
||||
Month,
|
||||
Quarter
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export enum Layouts{
|
||||
filmstrip = 0,
|
||||
compact = 1
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ICalendarEvent } from '../../../shared/models/ICalendarEvent';
|
||||
|
||||
export interface IReactMyEventsState {
|
||||
events: ICalendarEvent[];
|
||||
currentPage: number;
|
||||
loading: boolean;
|
||||
errorMessage: string;
|
||||
noEventsFoundMessage: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
});
|
|
@ -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 |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue