Merge from 'dev' to 'master'.

Merge from 'dev' to 'master'.
This commit is contained in:
Laura Kokkarinen 2019-08-24 20:44:42 +03:00 committed by GitHub
commit 481134e576
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 23803 additions and 4768 deletions

View File

@ -2,10 +2,10 @@
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.7.1",
"version": "1.9.1",
"libraryName": "react-calendar-feed",
"libraryId": "25653136-fc83-4abe-b9d2-a4ac041959d5",
"packageManager": "npm",
"componentType": "webpart"
}
}
}

View File

@ -2,7 +2,15 @@
## Summary
This web part uses RSS event feeds, iCal feeds, or WordPress calendar feeds and renders events using a look and feel that is consistent with the SharePoint out-of-the-box Group calendar/events web part.
This web part uses event feeds from various sources and renders events using a look and feel that is consistent with the SharePoint out-of-the-box Group calendar/events web part.
It supports the following types of feeds:
- iCal
- WordPress
- RSS
- Exchange Public Calendar
- SharePoint
![The web part in action](./assets/react-calendar-feed-demo.gif)
@ -16,21 +24,21 @@ For more information about how this solution was built, including some design de
## Used SharePoint Framework Version
![SPFx v1.7.1](https://img.shields.io/badge/SPFx-1.7.1-green.svg)
![SPFx v1.9.1](https://img.shields.io/badge/SPFx-1.9.1-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
- [SharePoint Framework](https:/dev.office.com/sharepoint)
- [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
Before you can use this web part example, you will need one of the following:
* A publicly-accessible iCal feed (i.e.: .ics)
* A publicly-accessible RSS feed of events (e.g.: Google calendar)
* A WordPress WP-FullCalendar feed
* An Exchange Public Calendar
- A publicly-accessible iCal feed (i.e.: .ics)
- A publicly-accessible RSS feed of events (e.g.: Google calendar)
- A WordPress WP-FullCalendar feed
- An Exchange Public Calendar
It is important that all feeds do not require authentication. Also, make sure that your calendar includes upcoming events, as the web part will filter out evens that are earlier than today's date.
@ -49,7 +57,8 @@ Version|Date|Comments
1.0|May 15, 2018|Initial release
2.0|June 25, 2018|Converted to SPFx 1.5 and added Exchange Public Calendar support
3.0|November 9, 2018|Converted to SPFx 1.7; Added SharePoint Calendar feed
4.0|January 16. 2019|Converted to SPFx 1.7.1; Removed NPM libraries associated with issue #708.
4.0|January 16, 2019|Converted to SPFx 1.7.1; Removed NPM libraries associated with issue #708.
5.0|August 17, 2019|Converted to SPFx 1.9.1; Refreshed carousel code; Addresses #735, #909. Also added **Convert from UTC** option to handle feeds which do not provide time zone information.
## Disclaimer
@ -59,32 +68,32 @@ Version|Date|Comments
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
* Insert the web part on a page
* When prompted to configure the web part, select **Configure** to launch the web part property pane.
* Select a feed type (RSS, iCal, WordPress, or Mock if using the debug solution)
* Provide the feed's URL. If using _Mock_, provide any valid URL (value will be ignored). If you wish to use a SharePoint calendar feed, provide the URL to the list (e.g.: https://yourtenant.sharepoint.com/sites/sitename/lists/eventlistname)
* Specify a date range (one week, two weeks, one month, one quarter, one year)
* Specify a maximum number of events to retrieve
* If necessary, specify to use a proxy. Use this option if you encounter issues where your feed provider does not accept your tenant URL as a CORS origin.
* If desired, specify how long (in minutes) you want to expire your users' local storage and refresh the events.
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
- Insert the web part on a page
- When prompted to configure the web part, select **Configure** to launch the web part property pane.
- Select a feed type (RSS, iCal, WordPress, or Mock if using the debug solution)
- Provide the feed's URL. If using _Mock_, provide any valid URL (value will be ignored). If you wish to use a SharePoint calendar feed, provide the URL to the list (e.g.: https://yourtenant.sharepoint.com/sites/sitename/lists/eventlistname)
- Specify a date range (one week, two weeks, one month, one quarter, one year)
- Specify a maximum number of events to retrieve
- If necessary, specify to use a proxy. Use this option if you encounter issues where your feed provider does not accept your tenant URL as a CORS origin.
- If desired, specify how long (in minutes) you want to expire your users' local storage and refresh the events.
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Rendering different views based on size
* Loading third-party CSS from a CDN
* Excluding mock data from production build
* Using @pnp/spfx-property-controls
* Using @pnp/spfx-controls-react
* Using localStorage to cache results locally
* Creating shared components and services
* Creating extensible services
* Using a proxy to resolve CORS issues
* Retrieving SharePoint events from a list with a filter
- Rendering different views based on size
- Loading third-party CSS from a CDN
- Excluding mock data from production build
- Using @pnp/spfx-property-controls
- Using @pnp/spfx-controls-react
- Using localStorage to cache results locally
- Creating shared components and services
- Creating extensible services
- Using a proxy to resolve CORS issues
- Retrieving SharePoint events from a list with a filter
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-calendar-feed" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"name": "react-calendar-feed",
"version": "1.1.0",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
@ -11,10 +12,10 @@
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.7.1",
"@microsoft/sp-lodash-subset": "1.7.1",
"@microsoft/sp-office-ui-fabric-core": "1.7.1",
"@microsoft/sp-webpart-base": "1.7.1",
"@microsoft/sp-core-library": "1.9.1",
"@microsoft/sp-lodash-subset": "1.9.1",
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
"@microsoft/sp-webpart-base": "1.9.1",
"@pnp/common": "^1.2.8",
"@pnp/logging": "^1.2.8",
"@pnp/odata": "^1.2.8",
@ -22,29 +23,31 @@
"@pnp/spfx-controls-react": "^1.11.0",
"@pnp/spfx-property-controls": "^1.13.1",
"@types/es6-promise": "0.0.33",
"@types/react": "16.4.2",
"@types/react-dom": "16.0.5",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"feedparser": "^2.2.9",
"ical.js": "^1.3.0",
"ics-js": "^0.10.2",
"react": "16.3.2",
"react-dom": "16.3.2",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5",
"react-slick": "^0.23.2",
"rss-parser": "^3.6.2",
"slick-carousel": "^1.8.1"
},
"resolutions": {
"@types/react": "16.4.2"
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.7.1",
"@microsoft/sp-tslint-rules": "1.7.1",
"@microsoft/sp-module-interfaces": "1.7.1",
"@microsoft/sp-webpart-workbench": "1.7.1",
"gulp": "~3.9.1",
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
"@microsoft/sp-build-web": "1.9.1",
"@microsoft/sp-module-interfaces": "1.9.1",
"@microsoft/sp-tslint-rules": "1.9.1",
"@microsoft/sp-webpart-workbench": "1.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

View File

@ -1,66 +0,0 @@
$white: '[theme:white,default:#ffffff]';
$black: '[theme:black,default:#000000]';
.carouselContainer.filmStrip {
margin: 0 -10px;
}
.carouselContainer.filmStrip:global(.slick-slide) {
box-sizing: border-box;
padding: 0 10px;
}
.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

@ -1,88 +0,0 @@
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

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

View File

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

View File

@ -1,10 +1,9 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
$neutralPrimary: '[theme:neutralPrimary,default:#333333]';
@import '~office-ui-fabric-react/dist/sass/References.scss';
.box {
font-weight: 400;
border: 1px solid;
color: $neutralPrimary;
color: $ms-color-neutralPrimary;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;

View File

@ -11,7 +11,9 @@ export class DateBox extends React.Component<IDateBoxProps, IDateBoxState> {
public render(): React.ReactElement<IDateBoxProps> {
// convert start and end date into moments so that we can manipulate them
const startMoment: moment.Moment = moment(this.props.startDate);
const endMoment: moment.Moment = moment(this.props.endDate);
// event actually ends one second before the end date
const endMoment: moment.Moment = moment(this.props.endDate).add(-1, "s");
// check if both dates are on the same day
const isSameDay: boolean = startMoment.isSame(endMoment, "day");

View File

@ -12,4 +12,4 @@ export interface IDateBoxState {
export enum DateBoxSize {
Small,
Medium
}
}

View File

@ -1,9 +1,4 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
$neutralLight: '[theme:neutralLight,default:#eaeaea]';
$neutralSecondary: '[theme:neutralSecondary,default:#666666]';
$white: '[theme:white,default:#ffffff]';
$neutralPrimary: '[theme:neutralPrimary,default:#333333]';
$neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
@import '~office-ui-fabric-react/dist/sass/References.scss';
.cardWrapper {
border: 1px solid;
@ -24,11 +19,11 @@ $neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
}
.cardWrapper:focus {
border-color: $neutralSecondary;
border-color: $ms-color-neutralSecondary;
}
.cardWrapper .dateBox {
border-color: $neutralTertiaryAlt;
border-color: $ms-color-neutralTertiaryAlt;
}
.normalCard .dateBoxContainer {
@ -81,11 +76,11 @@ $neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
.category,
.location {
color: $neutralPrimary;
color: $ms-color-neutralPrimary;
}
.datetime {
color: $neutralPrimary;
color: $ms-color-neutralPrimary;
}
.title {
@ -96,7 +91,7 @@ $neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
.addToMyCalendar,
.title {
font-weight: 400;
color: $neutralPrimary;
color: $ms-color-neutralPrimary;
}
.addToMyCalendar {
@ -116,7 +111,7 @@ $neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
width: 100%;
align-items: center;
-webkit-font-smoothing: antialiased;
background-color: $white;
background-color: $ms-color-white;
border: 1px solid #eaeaea;
-webkit-box-sizing: border-box;
box-sizing: border-box;
@ -143,11 +138,11 @@ $neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
}
.rootIsActionable:hover {
border-color: $neutralLight;
border-color: $ms-color-neutralLight;
}
.dateBox {
border-color: $neutralTertiaryAlt;
border-color: $ms-color-neutralTertiaryAlt;
}
.normalCard .title {
@ -167,7 +162,7 @@ $neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
.category,
.location {
color:$neutralSecondary;
color:$ms-color-neutralSecondary;
}
.category,
@ -182,7 +177,7 @@ $neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
height: 18px;
}
:global(.slick-slide) .cardWrapper {
padding-left:8px;
padding-right:8px;
}
// :global(.slick-slide) .cardWrapper {
// padding-left:8px;
// padding-right:8px;
// }

View File

@ -1,3 +1,5 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.Paging {
width: 100%;
min-width: 240px;

View File

@ -0,0 +1,89 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
:export {
centerPadding: 50px;
}
.filmstripLayout {
position: relative;
&.filmStrip {
margin: 0 -10px;
:global(.slick-slide) {
box-sizing: border-box;
padding: 0 10px;
}
}
.sliderButtons {
opacity: 0;
}
.sliderButtonRight {
right: 10px;
}
.sliderButtonLeft {
left: 10px;
}
&:hover .sliderButtons {
opacity: 1;
&:hover {
color: $ms-color-white;
}
}
}
.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: $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;
}
&:active {
outline: -webkit-focus-ring-color auto 1px;
}
}
.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,114 @@
import { css } from '@uifabric/utilities/lib/css';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import * as React from 'react';
// import * as slick from 'slick-carousel';
import Slider from 'react-slick';
import { IFilmstripLayoutProps, IFilmstripLayoutState } from "./FilmstripLayout.types";
import { SPComponentLoader } from '@microsoft/sp-loader';
import styles from "./FilmstripLayout.module.scss";
/**
* Filmstrip layout
* Presents the child compoments as a slick slide
*/
export class FilmstripLayout extends React.Component<
IFilmstripLayoutProps,
IFilmstripLayoutState
> {
// the slick slider used in normal views
private _slider: Slider;
/**
*
*/
constructor(props: IFilmstripLayoutProps) {
super(props);
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick.min.css');
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick-theme.min.css');
}
/**
* Renders a slick switch, a slide for each child, and next/previous arrows
*/
public render(): React.ReactElement<IFilmstripLayoutProps> {
// 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: styles.centerPadding,
pauseOnHover: true,
variableWidth: false,
useCSS: true,
rows: 1,
respondTo: "slider",
responsive: [
{
breakpoint: 499,
settings: {
slidesToShow: 1,
slidesToScroll: 1
}
},
{
breakpoint: 731,
settings: {
slidesToShow: 2,
slidesToScroll: 2
}
},
{
breakpoint: 963,
settings: {
slidesToShow: 3,
slidesToScroll: 3
}
},
{
breakpoint: 1028,
settings: {
slidesToShow: 4,
slidesToScroll: 4
}
}
]
};
return (
<div>
<div className={css(styles.filmstripLayout, styles.filmStrip)} aria-label={this.props.ariaLabel}>
<Slider ref={c => (this._slider = c)} {...settings}>
{this.props.children}
</Slider>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons, styles.sliderButtonLeft)}
onClick={() => this._slider.slickPrev()}
>
<IconButton
className={css(styles.indexButton, styles.leftPositioned)}
iconProps={{ iconName: "ChevronLeft" }}
/>
</div>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons, styles.sliderButtonRight)}
onClick={() => this._slider.slickNext()}
>
<IconButton
className={css(styles.indexButton, styles.rightPositioned)}
iconProps={{ iconName: "ChevronRight" }}
/>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,5 @@
export interface IFilmstripLayoutProps {
ariaLabel?: string;
}
export interface IFilmstripLayoutState { }

View File

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

View File

@ -12,93 +12,131 @@ import { ICalendarService } from "./ICalendarService";
* choose to do so. We won't judge.
*/
export abstract class BaseCalendarService implements ICalendarService {
public Context: IWebPartContext;
public FeedUrl: string;
public EventRange: CalendarEventRange;
public UseCORS: boolean;
public CacheDuration: number;
public Name: string;
public Context: IWebPartContext;
public FeedUrl: string;
public EventRange: CalendarEventRange;
public UseCORS: boolean;
public CacheDuration: number;
public Name: string;
public MaxTotal: number;
public ConvertFromUTC: boolean;
public getEvents: () => Promise<ICalendarEvent[]>;
/**
* Solves an issue where some providers (I'm looking at you, WordPress) returns all-day events
* as starting from midight on the first day, and ending at midnight on the second day, making events
* appear as lasting 2 days when they should last only 1 day
* @param event The event that needs to be fixed
*/
protected fixAllDayEvents(events: ICalendarEvent[]): ICalendarEvent[] {
events.forEach((event: ICalendarEvent) => {
if (event.allDay) {
const startMoment: moment.Moment = moment(event.start);
const endMoment: moment.Moment = moment(event.end).add(-1, "minute");
public getEvents: () => Promise<ICalendarEvent[]>;
/**
* Solves an issue where some providers (I'm looking at you, WordPress) returns all-day events
* as starting from midight on the first day, and ending at midnight on the second day, making events
* appear as lasting 2 days when they should last only 1 day
* @param event The event that needs to be fixed
*/
protected fixAllDayEvents(events: ICalendarEvent[]): ICalendarEvent[] {
events.forEach((event: ICalendarEvent) => {
if (event.allDay) {
const startMoment: moment.Moment = moment(event.start);
const endMoment: moment.Moment = moment(event.end).add(-1, "minute");
if (startMoment.isSame(endMoment, "day")) {
event.end = event.start;
}
}
return event;
});
return events;
if (startMoment.isSame(endMoment, "day")) {
event.end = event.start;
}
}
return event;
});
return events;
}
/**
* Not every provider allows the feed to be filtered. Use this method to filter events after
* the provider has retrieved them so that we can be consistent regardless of the provider
* @param events The list of events to filter
*/
protected filterEventRange(events: ICalendarEvent[]): ICalendarEvent[] {
const {
Start,
End } = this.EventRange;
// not all providers are good at (or capable of) filtering by events, let's just filter out events that fit outside the range
events = events.filter(e => e.start >= Start && e.end <= End);
// sort events by date in case we need to truncate
events.sort((leftSide: ICalendarEvent, rightSide: ICalendarEvent): number => {
if (leftSide.start < rightSide.start) {
return -1;
}
if (leftSide.start > rightSide.start) {
return 1;
}
return 0;
});
return events;
}
/**
* This is a cheesy approach to inject start and end dates from a feed url.
*/
protected replaceTokens(feedUrl: string, dateRange: CalendarEventRange): string {
const startMoment: moment.Moment = moment(dateRange.Start);
const startDate: string = startMoment.format("YYYY-MM-DD");
const endDate: string = startMoment.format("YYYY-MM-DD");
return feedUrl.replace("{s}", startDate)
.replace("{e}", endDate);
}
/**
* Retrieves the response using a CORS proxy or directly, depending on the settings
* @param feedUrl The URL where to retrieve the events
*/
protected fetchResponse(feedUrl: string): Promise<HttpClientResponse> {
// would love to use a different approach to workaround CORS issues
const requestUrl: string = this.getCORSUrl(feedUrl);
return this.Context.httpClient.fetch(requestUrl,
HttpClient.configurations.v1, {});
}
/**
* Returns a URL or a CORS-formatted URL
* @param feedUrl The URL for the feed
*/
protected getCORSUrl(feedUrl: string): string {
// would love to use a different approach to workaround CORS issues
return this.UseCORS ?
`https://cors-anywhere.herokuapp.com/${feedUrl}` :
feedUrl;
}
/**
* Retrives the response and returns a JSON object
* @param feedUrl The URL where to retrieve the events
*/
protected async fetchResponseAsJson(feedUrl: string): Promise<any> {
try {
const response = await this.fetchResponse(feedUrl);
return await response.json();
}
catch (error) {
throw error;
}
}
/**
* Converts a value to a date, possibly as a UTC date
* @param dateValue The date value to convert
*/
protected convertToDate(dateValue: any): Date {
let returnDate: Date = new Date(dateValue);
if (this.ConvertFromUTC) {
returnDate = new Date(returnDate.getUTCFullYear(),
returnDate.getUTCMonth(),
returnDate.getUTCDate(),
returnDate.getUTCHours(),
returnDate.getUTCMinutes(),
returnDate.getUTCSeconds()
);
}
/**
* Not every provider allows the feed to be filtered. Use this method to filter events after
* the provider has retrieved them so that we can be consistent regardless of the provider
* @param events The list of events to filter
*/
protected filterEventRange(events: ICalendarEvent[]): ICalendarEvent[] {
const { Start,
End } = this.EventRange;
// not all providers are good at (or capable of) filtering by events, let's just filter out events that fit outside the range
events = events.filter(e => e.start >= Start && e.end <= End);
return events;
}
/**
* This is a cheesy approach to inject start and end dates from a feed url.
*/
protected replaceTokens(feedUrl: string, dateRange: CalendarEventRange): string {
const startMoment: moment.Moment = moment(dateRange.Start);
const startDate: string = startMoment.format("YYYY-MM-DD");
const endDate: string = startMoment.format("YYYY-MM-DD");
return feedUrl.replace("{s}", startDate)
.replace("{e}", endDate);
}
/**
* Retrieves the response using a CORS proxy or directly, depending on the settings
* @param feedUrl The URL where to retrieve the events
*/
protected fetchResponse(feedUrl: string): Promise<HttpClientResponse> {
// would love to use a different approach to workaround CORS issues
const requestUrl: string = this.getCORSUrl(feedUrl);
return this.Context.httpClient.fetch(requestUrl,
HttpClient.configurations.v1, {});
}
/**
* Returns a URL or a CORS-formatted URL
* @param feedUrl The URL for the feed
*/
protected getCORSUrl(feedUrl: string): string {
// would love to use a different approach to workaround CORS issues
return this.UseCORS ?
`https://cors-anywhere.herokuapp.com/${feedUrl}` :
feedUrl;
}
/**
* Retrives the response and returns a JSON object
* @param feedUrl The URL where to retrieve the events
*/
protected fetchResponseAsJson(feedUrl: string): Promise<any> {
return this.fetchResponse(feedUrl)
.then((response: HttpClientResponse) => response.json(), (error: any) => {
throw error;
});
}
return returnDate;
}
}

View File

@ -7,4 +7,4 @@ export interface ICalendarEvent {
category: string|undefined;
description: string|undefined;
location: string|undefined;
}
}

View File

@ -7,6 +7,8 @@ export interface ICalendarService {
EventRange: CalendarEventRange;
UseCORS: boolean;
CacheDuration: number;
MaxTotal: number;
ConvertFromUTC: boolean;
Name: string;
getEvents: () => Promise<ICalendarEvent[]>;
}

View File

@ -9,149 +9,149 @@ import { ICalendarEvent } from "../ICalendarEvent";
import { ICalendarService } from "../ICalendarService";
const sampleEvents: ICalendarEvent[] = [
{
"title": "This event will be tomorrow",
"start": moment().add(1, "d").toDate(),
"end": moment().add(1, "d").toDate(),
"url": "https://www.contoso.com/news-events/events/1/",
"allDay": true,
"category": "Meeting",
"location": "Barrie, ON",
"description": "This is a description"
},
{
"title": "This event will be in one week",
"start": moment().add(1, "w").toDate(),
"end": moment().add(1, "w").add(1, "h").toDate(),
"url": "https://www.contoso.com/news-events/events/2/",
"allDay": false,
"category": "Meeting",
"location": undefined,
"description": undefined
},
{
"title": "This event will last two days",
"start": moment().add(1, "w").toDate(),
"end": moment().add(1, "w").add(2, "d").toDate(),
"url": "https://www.contoso.com/news-events/events/2/",
"allDay": true,
"category": "Meeting",
"location": undefined,
"description": undefined
},
{
"title": "This event will be in two weeks",
"start": moment().add(2, "w").toDate(),
"end": moment().add(2, "w").toDate(),
"url": "https://www.contoso.com/news-events/events/3/",
"allDay": true,
"category": "Meeting",
"location": undefined,
"description": undefined
},
{
"title": "This event will be in one month",
"start": moment().add(1, "M").toDate(),
"end": moment().add(1, "M").add(2, "d").toDate(),
"url": "https://www.contoso.com/news-events/events/4/",
"allDay": true,
"category": "Meeting",
"location": undefined,
"description": undefined
},
{
"title": "This event will be in two months",
"start": moment().add(2, "M").toDate(),
"end": moment().add(2, "M").toDate(),
"url": "https://www.contoso.com/news-events/events/5/",
"allDay": true,
"category": "Meeting",
"location": undefined,
"description": undefined
},
{
"title": "This event will be in 1 quarter",
"start": moment().add(1, "Q").toDate(),
"end": moment().add(1, "Q").toDate(),
"url": "https://www.contoso.com/news-events/events/6/",
"allDay": true,
"category": undefined,
"location": undefined,
"description": undefined
},
{
"title": "This event will be in 4 months",
"start": moment().add(4, "M").toDate(),
"end": moment().add(4, "M").toDate(),
"url": "https://www.contoso.com/news-events/events/7/",
"allDay": true,
"category": undefined,
"location": undefined,
"description": undefined
},
{
"title": "This event will be in 5 months",
"start": moment().add(5, "M").toDate(),
"end": moment().add(5, "M").toDate(),
"url": "https://www.contoso.com/news-events/events/8/",
"allDay": true,
"category": undefined,
"location": undefined,
"description": undefined
},
{
"title": "This event will be in 6 months",
"start": moment().add(6, "M").toDate(),
"end": moment().add(6, "M").toDate(),
"url": "https://www.contoso.com/news-events/events/9/",
"allDay": true,
"category": undefined,
"location": undefined,
"description": undefined
},
{
"title": "This event will be in 9 months",
"start": moment().add(9, "M").toDate(),
"end": moment().add(9, "M").toDate(),
"url": "https://www.contoso.com/news-events/events/10/",
"allDay": true,
"category": undefined,
"location": undefined,
"description": undefined
},
{
"title": "This event will be in 1 year",
"start": moment().add(1, "y").toDate(),
"end": moment().add(1, "y").toDate(),
"url": "https://www.contoso.com/news-events/events/11/",
"allDay": true,
"category": "Partayyyy!",
"location": undefined,
"description": undefined
},
{
"title": "This event will be in 18 months",
"start": moment().add(18, "M").toDate(),
"end": moment().add(18, "M").toDate(),
"url": "https://www.contoso.com/news-events/events/12/",
"allDay": true,
"category": "Meeting",
"location": undefined,
"description": undefined
}
{
title: "This event will be tomorrow",
start: moment().add(1, "d").toDate(),
end: moment().add(1, "d").toDate(),
url: "https://www.contoso.com/news-events/events/1/",
allDay: true,
category: "Meeting",
location: "Barrie, ON",
description: "This is a description"
},
{
title: "This event will be in one week",
start: moment().add(1, "w").toDate(),
end: moment().add(1, "w").add(1, "h").toDate(),
url: "https://www.contoso.com/news-events/events/2/",
allDay: false,
category: "Meeting",
location: undefined,
description: undefined
},
{
title: "This event will last two days",
start: moment().add(1, "w").toDate(),
end: moment().add(1, "w").add(2, "d").toDate(),
url: "https://www.contoso.com/news-events/events/2/",
allDay: true,
category: "Meeting",
location: undefined,
description: undefined
},
{
title: "This event will be in two weeks",
start: moment().add(2, "w").toDate(),
end: moment().add(2, "w").toDate(),
url: "https://www.contoso.com/news-events/events/3/",
allDay: true,
category: "Meeting",
location: undefined,
description: undefined
},
{
title: "This event will be in one month",
start: moment().add(1, "M").toDate(),
end: moment().add(1, "M").add(2, "d").toDate(),
url: "https://www.contoso.com/news-events/events/4/",
allDay: true,
category: "Meeting",
location: undefined,
description: undefined
},
{
title: "This event will be in two months",
start: moment().add(2, "M").toDate(),
end: moment().add(2, "M").toDate(),
url: "https://www.contoso.com/news-events/events/5/",
allDay: true,
category: "Meeting",
location: undefined,
description: undefined
},
{
title: "This event will be in 1 quarter",
start: moment().add(1, "Q").toDate(),
end: moment().add(1, "Q").toDate(),
url: "https://www.contoso.com/news-events/events/6/",
allDay: true,
category: undefined,
location: undefined,
description: undefined
},
{
title: "This event will be in 4 months",
start: moment().add(4, "M").toDate(),
end: moment().add(4, "M").toDate(),
url: "https://www.contoso.com/news-events/events/7/",
allDay: true,
category: undefined,
location: undefined,
description: undefined
},
{
title: "This event will be in 5 months",
start: moment().add(5, "M").toDate(),
end: moment().add(5, "M").toDate(),
url: "https://www.contoso.com/news-events/events/8/",
allDay: true,
category: undefined,
location: undefined,
description: undefined
},
{
title: "This event will be in 6 months",
start: moment().add(6, "M").toDate(),
end: moment().add(6, "M").toDate(),
url: "https://www.contoso.com/news-events/events/9/",
allDay: true,
category: undefined,
location: undefined,
description: undefined
},
{
title: "This event will be in 9 months",
start: moment().add(9, "M").toDate(),
end: moment().add(9, "M").toDate(),
url: "https://www.contoso.com/news-events/events/10/",
allDay: true,
category: undefined,
location: undefined,
description: undefined
},
{
title: "This event will be in 1 year",
start: moment().add(1, "y").toDate(),
end: moment().add(1, "y").toDate(),
url: "https://www.contoso.com/news-events/events/11/",
allDay: true,
category: "Partayyyy!",
location: undefined,
description: undefined
},
{
title: "This event will be in 18 months",
start: moment().add(18, "M").toDate(),
end: moment().add(18, "M").toDate(),
url: "https://www.contoso.com/news-events/events/12/",
allDay: true,
category: "Meeting",
location: undefined,
description: undefined
}
];
export class MockCalendarService extends BaseCalendarService implements ICalendarService {
constructor() {
super();
this.Name = "Mock";
}
constructor() {
super();
this.Name = "Mock";
}
public getEvents = (): Promise<ICalendarEvent[]> => {
return new Promise<ICalendarEvent[]>((resolve: any) => {
setTimeout(() => {
resolve(this.filterEventRange(sampleEvents));
}, 1000);
});
}
public getEvents = (): Promise<ICalendarEvent[]> => {
return new Promise<ICalendarEvent[]>((resolve: any) => {
setTimeout(() => {
resolve(this.filterEventRange(sampleEvents));
}, 1000);
});
}
}

View File

@ -24,8 +24,8 @@ export class RSSCalendarService extends BaseCalendarService implements ICalendar
return parser.parseURL(parameterizedFeedUrl).then(feed => {
let events: ICalendarEvent[] = feed.items.map(item => {
let pubDate: Date = new Date(item.isoDate);
return {
let pubDate: Date = this.convertToDate(item.isoDate);
const eventItem: ICalendarEvent = {
title: item.title,
start: pubDate,
end: pubDate,
@ -35,6 +35,7 @@ export class RSSCalendarService extends BaseCalendarService implements ICalendar
location: undefined, // no equivalent in RSS
category: item.categories && item.categories.length > 0 && item.categories[0]
};
return eventItem;
});
return events;
});

View File

@ -15,14 +15,14 @@ export class SharePointCalendarService extends BaseCalendarService
this.Name = "SharePoint";
}
public getEvents = (): Promise<ICalendarEvent[]> => {
public getEvents = async (): Promise<ICalendarEvent[]> => {
const parameterizedFeedUrl: string = this.replaceTokens(
this.FeedUrl,
this.EventRange
);
// Get the URL
let webUrl = this.FeedUrl.toLowerCase();
let webUrl = parameterizedFeedUrl.toLowerCase();
// Break the URL into parts
let urlParts = webUrl.split("/");
@ -41,42 +41,36 @@ export class SharePointCalendarService extends BaseCalendarService
let web = new Web(siteUrl);
// Get the web
return web.get().then(() => {
// Build a filter so that we don't retrieve every single thing unless necesssary
let dateFilter:string = "EventDate ge datetime'"+this.EventRange.Start.toISOString()+"' and EndDate lt datetime'"+this.EventRange.End.toISOString()+"'";
// When we receive the web, get the list
return web
.getList(listUrl)
await web.get();
// Build a filter so that we don't retrieve every single thing unless necesssary
let dateFilter: string = "EventDate ge datetime'" + this.EventRange.Start.toISOString() + "' and EndDate lt datetime'" + this.EventRange.End.toISOString() + "'";
try {
const items = await web.getList(listUrl)
.items.select("Id,Title,Description,EventDate,EndDate,fAllDayEvent,Category,Location")
.orderBy('EventDate', true)
.filter(dateFilter)
.getAll()
.then((items: any[]) => {
// Once we get the list, convert to calendar events
let events: ICalendarEvent[] = items.map((item: any) => {
let eventUrl:string = combine(webUrl, "DispForm.aspx?ID="+item.Id);
return {
title: item.Title,
start: item.EventDate,
end: item.EndDate,
url: eventUrl,
allDay: item.fAllDayEvent,
category: item.Category,
description: item.Description,
location: item.Location
};
});
// Return the calendar items
return events;
})
.catch((error: any) => {
console.log(
"Exception caught by catch in SharePoint provider",
error
);
throw error;
});
});
.get();
// Once we get the list, convert to calendar events
let events: ICalendarEvent[] = items.map((item: any) => {
let eventUrl: string = combine(webUrl, "DispForm.aspx?ID=" + item.Id);
const eventItem: ICalendarEvent = {
title: item.Title,
start: item.EventDate,
end: item.EndDate,
url: eventUrl,
allDay: item.fAllDayEvent,
category: item.Category,
description: item.Description,
location: item.Location
};
return eventItem;
});
// Return the calendar items
return events;
}
catch (error) {
console.log("Exception caught by catch in SharePoint provider", error);
throw error;
}
}
}

View File

@ -12,31 +12,32 @@ export class WordPressFullCalendarService extends BaseCalendarService implements
this.Name = "WordPress";
}
public getEvents = (): Promise<ICalendarEvent[]> => {
public getEvents = async (): Promise<ICalendarEvent[]> => {
const parameterizedFeedUrl: string = this.replaceTokens(this.FeedUrl, this.EventRange);
return this.fetchResponseAsJson(parameterizedFeedUrl)
.then((data: IWordPressFullCalendarEventResponse[]): ICalendarEvent[] => {
let events: ICalendarEvent[] = data.map((e: IWordPressFullCalendarEventResponse) => {
return {
title: e.title,
start: new Date(e.start),
end: new Date(e.end),
url: e.url,
post_id: e.post_id,
event_id: e.event_id,
allDay: e.allDay,
description: undefined, // none found in WordPress
category: undefined, // none found in WordPress
location: undefined // none found in WordPress
};
});
return this.filterEventRange(this.fixAllDayEvents(events));
}).catch((error: any) => {
console.log("Exception caught by catch in WordPress provider", error);
throw error;
});
try {
const data = await this.fetchResponseAsJson(parameterizedFeedUrl);
let events: ICalendarEvent[] = data.map((e: IWordPressFullCalendarEventResponse) => {
const startDate: Date = this.convertToDate(e.start);
const endDate: Date = this.convertToDate(e.end);
const eventItem: ICalendarEvent = {
title: e.title,
start: startDate,
end: endDate,
url: e.url,
allDay: e.allDay,
description: undefined,
category: undefined,
location: undefined // none found in WordPress
};
return eventItem;
});
return this.filterEventRange(this.fixAllDayEvents(events));
}
catch (error) {
console.log("Exception caught by catch in WordPress provider", error);
throw error;
}
}
}

View File

@ -1,7 +1,6 @@
/**
* ExtensionService
*/
import { HttpClientResponse } from "@microsoft/sp-http";
import * as ICAL from "ical.js";
import { ICalendarService } from "..";
import { BaseCalendarService } from "../BaseCalendarService";
@ -9,42 +8,44 @@ import { ICalendarEvent } from "../ICalendarEvent";
// tslint:disable-next-line:class-name
export class iCalCalendarService extends BaseCalendarService implements ICalendarService {
constructor() {
super();
this.Name = "iCal";
constructor() {
super();
this.Name = "iCal";
}
public getEvents = async (): Promise<ICalendarEvent[]> => {
const parameterizedFeedUrl: string = this.replaceTokens(this.FeedUrl, this.EventRange);
try {
const response = await this.fetchResponse(parameterizedFeedUrl);
const data = await response.text();
const jsonified: any = ICAL.parse(data);
const comp: any = new ICAL.Component(jsonified);
const veventList: any[] = comp.getAllSubcomponents("vevent");
let events: ICalendarEvent[] = veventList.map((vevent: any) => {
const event: ICAL.Event = new ICAL.Event(vevent);
let startDate = this.convertToDate(event.startDate);
let endDate = this.convertToDate(event.endDate);
const eventItem: ICalendarEvent = {
title: event.summary,
start: startDate,
end: endDate,
url: event.url,
allDay: event.startDate.icaltype === "date",
category: event.category,
description: event.description,
location: event.location
};
return eventItem;
});
return this.filterEventRange(events);
}
public getEvents = (): Promise<ICalendarEvent[]> => {
const parameterizedFeedUrl: string = this.replaceTokens(this.FeedUrl, this.EventRange);
return this.fetchResponse(parameterizedFeedUrl)
.then((response: HttpClientResponse) => response.text())
.then((data: string) => {
let jsonified: any = ICAL.parse(data);
var comp: any = new ICAL.Component(jsonified);
var veventList: any[] = comp.getAllSubcomponents("vevent");
return veventList;
})
.then((data: any[]) => {
let events: ICalendarEvent[] = data.map((vevent: any) => {
var event: ICAL.Event = new ICAL.Event(vevent);
return {
title: event.summary,
start: new Date(event.startDate),
end: new Date(event.endDate),
url: event.url,
allDay: event.allDay,
category: event.category,
description: event.description,
location: event.location
};
});
return this.filterEventRange(events);
}).catch((error: any) => {
console.log("Exception caught by catch in iCal provider", error);
throw error;
});
catch (error) {
console.log("Exception caught by catch in iCal provider", error);
throw error;
}
}
}

View File

@ -6,7 +6,7 @@
"version": "*",
"manifestVersion": 2,
"supportedHosts": ["SharePointWebPart"],
"requiresCustomScript": false,
"preconfiguredEntries": [{
@ -26,7 +26,9 @@
"dateRange": 4,
"maxEvents": 4,
"useCORS": false,
"cacheDuration": 15
"cacheDuration": 15,
"maxTotal": 10,
"convertFromUTC": false
}
}]
}

View File

@ -1,13 +1,14 @@
import * as React from "react";
import * as ReactDom from "react-dom";
// SharePoint imports
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
IPropertyPaneDropdownOption,
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
PropertyPaneDropdown,
PropertyPaneToggle,
PropertyPaneLabel
} from "@microsoft/sp-property-pane";
// Needed for data versions
import { Version } from '@microsoft/sp-core-library';
@ -58,6 +59,8 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
let {
cacheDuration,
dateRange,
maxTotal,
convertFromUTC: convertFromUTC
} = this.properties;
// make sure to set a default date range if it isn't defined
@ -71,6 +74,14 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
cacheDuration = 15;
}
if (maxTotal === undefined) {
maxTotal = 0;
}
if (convertFromUTC === undefined) {
convertFromUTC = false;
}
resolve(undefined);
});
}
@ -127,7 +138,9 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
maxEvents,
useCORS,
cacheDuration,
feedType
feedType,
maxTotal,
convertFromUTC
} = this.properties;
const isMock: boolean = feedType === CalendarServiceProviderType.Mock;
@ -179,14 +192,19 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
groupName: strings.AdvancedGroupName,
isCollapsed: true,
groupFields: [
// how many items are we diplaying in a page
PropertyFieldNumber("maxEvents", {
key: "maxEventsFieldId",
label: strings.MaxEventsFieldLabel,
description: strings.MaxEventsFieldDescription,
value: maxEvents,
minValue: 0,
disabled: false
PropertyPaneLabel('convertFromUTC', {
text: strings.ConvertFromUTCFieldDescription
}),
// Convert from UTC toggle
PropertyPaneToggle("convertFromUTC", {
key: "convertFromUTCFieldId",
label: strings.ConvertFromUTCLabel,
onText: strings.ConvertFromUTCOptionYes,
offText: strings.ConvertFromUTCOptionNo,
checked: convertFromUTC,
}),
PropertyPaneLabel('useCORS', {
text: strings.UseCorsFieldDescription
}),
// use CORS toggle
PropertyFieldToggleWithCallout("useCORS", {
@ -194,8 +212,8 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
calloutTrigger: CalloutTriggers.Hover,
key: "useCORSFieldId",
label: strings.UseCORSFieldLabel,
calloutWidth: 200,
calloutContent: React.createElement("div", {}, isMock ? strings.UseCORSFieldCalloutDisabled : strings.UseCORSFieldCallout),
//calloutWidth: 200,
calloutContent: React.createElement("p", {}, isMock ? strings.UseCORSFieldCalloutDisabled : strings.UseCORSFieldCallout),
onText: strings.CORSOn,
offText: strings.CORSOff,
checked: useCORS
@ -212,6 +230,23 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
step: 15,
showValue: true,
value: cacheDuration
}),
// how many items are we diplaying in a page
PropertyFieldNumber("maxEvents", {
key: "maxEventsFieldId",
label: strings.MaxEventsFieldLabel,
description: strings.MaxEventsFieldDescription,
value: maxEvents,
minValue: 0,
disabled: false
}),
PropertyFieldNumber("maxTotal", {
key: "maxTotalFieldId",
label: strings.MaxTotalFieldLabel,
description: strings.MaxTotalFieldDescription,
value: maxTotal,
minValue: 0,
disabled: false
})
],
}
@ -293,7 +328,9 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
const {
feedUrl,
useCORS,
cacheDuration
cacheDuration,
convertFromUTC,
maxTotal
} = this.properties;
// get the first provider matching the type selected
@ -314,6 +351,8 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
provider.UseCORS = useCORS;
provider.CacheDuration = cacheDuration;
provider.EventRange = new CalendarEventRange(this.properties.dateRange);
provider.ConvertFromUTC = convertFromUTC;
provider.MaxTotal = maxTotal;
return provider;
}
}

View File

@ -4,11 +4,48 @@ import { DateRange, CalendarServiceProviderType } from "../../shared/services/Ca
* Web part properties stored in web part configuration
*/
export interface ICalendarFeedSummaryWebPartProps {
title: string; // title of the web part
feedUrl: string; // the URL where to get the feed from
feedType: CalendarServiceProviderType; // the type of feed provider
maxEvents: number; // maximum number of events
dateRange: DateRange; // date range to retrieve events
useCORS: boolean; // use CORS proxy when retrieving events
cacheDuration: number; // how long to cache events for
}
/**
* The title of the web part
*/
title: string;
/**
* The URL where to get the feed from
*/
feedUrl: string;
/**
* The type of feed provider
*/
feedType: CalendarServiceProviderType;
/**
* maximum number of events per page
*/
maxEvents: number;
/**
* Maximum total number of events to load
*/
maxTotal: number;
/**
* Date range to retrieve events
*/
dateRange: DateRange;
/**
* use CORS proxy when retrieving events
*/
useCORS: boolean;
/**
* how long to cache events for
*/
cacheDuration: number;
/**
* Indicates the dates received from feeds do not specify a timezone
*/
convertFromUTC: boolean;
}

View File

@ -1,17 +1,10 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
$themeWhite: '[theme:white,default:white]';
$neutralLight: '[theme:neutralLight,default:#eaeaea]';
$neutralLighter: '[theme:neutralLighter,default:#f4f4f4]';
$neutralSecondary: '[theme:neutralSecondary,default:#666666]';
$black: '[theme:black,default:#000000]';
$white: '[theme:white,default:#ffffff]';
$themeLight: '[theme:themeLight,default:#c7e0f4]';
$themeSecondary: '[theme:themeSecondary,default:#2b88d8]';
$neutralPrimary: '[theme:neutralPrimary,default:#333333]';
$neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
$error: "[theme:error, default: #a80000]";
@import '~office-ui-fabric-react/dist/sass/References.scss';
.calendarFeedSummary {
// this is a trick I use to create classes that are empty
// just define an attribute which uses 'inherit'
// it has no impact to styles, but won't complain
// about the empty CSS class
color: inherit;
}
@ -63,16 +56,16 @@ $error: "[theme:error, default: #a80000]";
overflow: hidden;
display: inline-block;
margin: 10px 0 15px;
color: #666666;
color: $ms-color-neutralSecondary;
}
.errorMessage {
overflow: hidden;
display: inline-block;
margin: 10px 0 15px;
color: #666666;
color: $ms-color-neutralSecondary;
}
.errorMessage .moreDetails {
color:$error;
}
color:$ms-color-error;
}

View File

@ -6,12 +6,12 @@ import * as strings from "CalendarFeedSummaryWebPartStrings";
import * as moment from "moment";
import { FocusZone, FocusZoneDirection, List, Spinner, css } from "office-ui-fabric-react";
import * as React from "react";
import { CarouselContainer } from "../../../shared/components/CarouselContainer";
import { EventCard } from "../../../shared/components/EventCard";
import { Paging } from "../../../shared/components/Paging";
import { CalendarServiceProviderType, ICalendarEvent, ICalendarService } from "../../../shared/services/CalendarService";
import styles from "./CalendarFeedSummary.module.scss";
import { ICalendarFeedSummaryProps, ICalendarFeedSummaryState, IFeedCache } from "./CalendarFeedSummary.types";
import { FilmstripLayout } from "../../../shared/components/filmstripLayout/index";
// the key used when caching events
const CacheKey: string = "calendarFeedSummary";
@ -28,10 +28,6 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
error: undefined,
currentPage: 1
};
// needed for the slick slider in normal mode
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");
}
/**
@ -73,7 +69,9 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
prevProvider.FeedUrl !== currProvider.FeedUrl ||
prevProvider.Name !== currProvider.Name ||
prevProvider.EventRange.DateRange !== currProvider.EventRange.DateRange ||
prevProvider.UseCORS !== currProvider.UseCORS;
prevProvider.UseCORS !== currProvider.UseCORS ||
prevProvider.MaxTotal !== currProvider.MaxTotal ||
prevProvider.ConvertFromUTC !== currProvider.ConvertFromUTC;
if (settingsHaveChanged) {
// only load from cache if the providers haven't changed, otherwise reload.
@ -289,7 +287,9 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
return (<div>
<div>
<div role="application">
<CarouselContainer >
<FilmstripLayout
ariaLabel={strings.FilmStripAriaLabel}
>
{events.map((event: ICalendarEvent, index: number) => {
return (<EventCard
key={`eventCard${index}`}
@ -297,7 +297,7 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
event={event}
isNarrow={false} />);
})}
</CarouselContainer>
</FilmstripLayout>
</div>
</div>
</div>);
@ -313,7 +313,7 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
/**
* Load events from the cache or, if expired, load from the event provider
*/
private _loadEvents(useCacheIfPossible: boolean): Promise<void> {
private async _loadEvents(useCacheIfPossible: boolean): Promise<void> {
// before we do anything with the data provider, let's make sure that we don't have stuff stored in the cache
// load from cache if: 1) we said to use cache, and b) if we have something in cache
@ -340,7 +340,11 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
isLoading: true
});
return dataProvider.getEvents().then((events: ICalendarEvent[]) => {
try {
let events = await dataProvider.getEvents();
if (dataProvider.MaxTotal > 0) {
events = events.slice(0, dataProvider.MaxTotal);
}
// don't cache in the case of errors
this.setState({
isLoading: false,
@ -348,14 +352,15 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
events: events
});
return;
}).catch((error: any) => {
}
catch (error) {
console.log("Exception returned by getEvents", error.message);
this.setState({
isLoading: false,
error: error.message,
events: []
});
});
}
}
}
}

View File

@ -1,58 +1,66 @@
define([], function() {
return {
"PropertyPaneDescription": "Select the type of feed you wish to connect to and the feed URL.",
"AllItemsUrlFieldLabel": "View all events URL",
"FeedUrlFieldLabel": "Feed URL",
"FeedTypeFieldLabel": "Feed type",
"PlaceholderTitle": "Configure event feed",
"PlaceholderDescription": "To display a summary of events, you need to select a feed type and configure the event feed URL.",
"ConfigureButton": "Configure",
"FeedTypeOptionGoogle": "Google Calendar",
"FeedTypeOptioniCal": "iCal",
"FeedTypeOptionRSS": "RSS Calendar",
"FeedTypeOptionWordPress": "WordPress WP_FullCalendar",
"FeedUrlCallout": "If your feed supports date range parameters, use {s} and {e} for start and end dates, and we'll replace them with date values.",
"MaxEventsFieldLabel": "Maximum number of events per page",
"MaxEventsFieldDescription": "Indicates the number of events to show per page when displaying a narrow list. Use 0 for no maximum",
"DateRangeFieldLabel": "Date range",
"DateRangeOptionUpcoming": "Next year",
"DateRangeOptionWeek": "Next week",
"DateRangeOptionTwoWeeks": "Next two weeks",
"DateRangeOptionMonth": "Next month",
"DateRangeOptionQuarter": "Next quarter",
"UseCORSFieldLabel": "Use proxy",
"UseCORSFieldCallout": "Enable this option if you get a CORS message",
"UseCORSFieldCalloutDisabled": "This option is disabled when using the Mock provider",
"CORSOn": "On",
"CORSOff": "Off",
"AdvancedGroupName": "Advanced",
"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.",
"EventCardWrapperArialLabel": "Event {0}. Start on {1}.",
"Loading": "Please wait...",
"NoEventsMessage": "There aren't any upcoming events.",
"CacheDurationFieldLabel": "Cache duration (minutes)",
"CacheDurationFieldCallout": "Use 0 if you do not want to cache events. Maximum value is 1 day (14,400 minutes).",
"FeedUrlValidationNoUrl": "Provide a URL",
"FeedUrlValidationInvalidFormat": "URL is not a valid format. Please use a URL that starts with http:// or https://. ",
"ErrorMessage": "Oops, something went wrong! We can't display your events at the moment. Please try again later.",
"NextButtonLabel": "Next",
"PrevButtonLabel": "Previous",
"NextButtonAriaLabel": "Go to the Next page",
"PrevButtonAriaLabel": "Go to the Previous page",
"ErrorNotFound":"The feed URL you specified cannot be found. Make sure that you have the right URL and try again.",
"ErrorMixedContent": "Failed to fetch the feed URL you specified. Try using an https:// URL, or enable the \"Use proxy\" option",
"ErrorFailedToFetch": "Failed to fetch the feed URL you specified. This may be due to an invalid URL.",
"ErrorFailedToFetchNoProxy": "Failed to fetch the feed URL you specified. This may be due to an invalid URL or a CORS issue. Verify the URL, or enable the \"Use proxy\" option.",
"ErrorRssNoResult":"The feed you specified does not appear to be a RSS feed",
"ErrorRssNoRoot": "The RSS feed you specified appear to be invalid: it does not have a root",
"ErrorRssNoChannel": "The RSS feed you specified appear to be invalid: it does not have a channel",
"ErrorInvalidiCalFeed": "The URL you provided does not appear to be an iCal feed. Are you sure you selected the right feed type?",
"ErrorInvalidWordPressFeed": "The URL you provided does not appear to be a WordPress feed. Are you sure you selected the right feed type?",
"AddToCalendarAriaLabel": "Press enter to download the calendar file to your device.",
"AddToCalendarButtonLabel": "Add to my calendar",
"AllDayDateFormat": "dddd, MMMM Do YYYY",
"LocalizedTimeFormat": "llll",
"FeedSettingsGroupName": "Calendar feed"
UseCorsFieldDescription: "If you get a message saying \"Failed to fetch the feed URL you specified.\", try enabling the \"Use proxy\" option.",
ConvertFromUTCFieldDescription: "If your feed returns Universal Time Coordinated (UTC) events and your events appear at the wrong time zone, try enabling \"Convert from UTC\".",
ConvertFromUTCOptionNo: "Do not convert",
ConvertFromUTCOptionYes: "Convert",
ConvertFromUTCLabel: "Convert from UTC",
MaxTotalFieldDescription: "Indicates the total number of events to load. Use 0 for no maximum.",
MaxTotalFieldLabel: "Maximum number of events",
FilmStripAriaLabel: "Events list. Use left and right arrow keys to move between events. Press enter to go to the selected event.",
PropertyPaneDescription: "Select the type of feed you wish to connect to and the feed URL.",
AllItemsUrlFieldLabel: "View all events URL",
FeedUrlFieldLabel: "Feed URL",
FeedTypeFieldLabel: "Feed type",
PlaceholderTitle: "Configure event feed",
PlaceholderDescription: "To display a summary of events, you need to select a feed type and configure the event feed URL.",
ConfigureButton: "Configure",
FeedTypeOptionGoogle: "Google Calendar",
FeedTypeOptioniCal: "iCal",
FeedTypeOptionRSS: "RSS Calendar",
FeedTypeOptionWordPress: "WordPress WP_FullCalendar",
FeedUrlCallout: "If your feed supports date range parameters, use {s} and {e} for start and end dates, and we'll replace them with date values.",
MaxEventsFieldLabel: "Narrow page length",
MaxEventsFieldDescription: "Indicates the number of events to show per page when displaying a narrow list. Use 0 for no maximum",
DateRangeFieldLabel: "Date range",
DateRangeOptionUpcoming: "Next year",
DateRangeOptionWeek: "Next week",
DateRangeOptionTwoWeeks: "Next two weeks",
DateRangeOptionMonth: "Next month",
DateRangeOptionQuarter: "Next quarter",
UseCORSFieldLabel: "Use proxy",
UseCORSFieldCallout: "Enable this option if you get a CORS message",
UseCORSFieldCalloutDisabled: "This option is disabled when using the Mock provider",
CORSOn: "On",
CORSOff: "Off",
AdvancedGroupName: "Advanced",
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.",
EventCardWrapperArialLabel: "Event {0}. Start on {1}.",
Loading: "Please wait...",
NoEventsMessage: "There aren't any upcoming events.",
CacheDurationFieldLabel: "Cache duration (minutes)",
CacheDurationFieldCallout: "Use 0 if you do not want to cache events. Maximum value is 1 day (14,400 minutes).",
FeedUrlValidationNoUrl: "Provide a URL",
FeedUrlValidationInvalidFormat: "URL is not a valid format. Please use a URL that starts with http:// or https://. ",
ErrorMessage: "Oops, something went wrong! We can't display your events at the moment. Please try again later.",
NextButtonLabel: "Next",
PrevButtonLabel: "Previous",
NextButtonAriaLabel: "Go to the Next page",
PrevButtonAriaLabel: "Go to the Previous page",
ErrorNotFound:"The feed URL you specified cannot be found. Make sure that you have the right URL and try again.",
ErrorMixedContent: "Failed to fetch the feed URL you specified. Try using an https:// URL, or enable the \"Use proxy\" option",
ErrorFailedToFetch: "Failed to fetch the feed URL you specified. This may be due to an invalid URL.",
ErrorFailedToFetchNoProxy: "Failed to fetch the feed URL you specified. This may be due to an invalid URL or a CORS issue. Verify the URL, or enable the \"Use proxy\" option.",
ErrorRssNoResult:"The feed you specified does not appear to be a RSS feed",
ErrorRssNoRoot: "The RSS feed you specified appear to be invalid: it does not have a root",
ErrorRssNoChannel: "The RSS feed you specified appear to be invalid: it does not have a channel",
ErrorInvalidiCalFeed: "The URL you provided does not appear to be an iCal feed. Are you sure you selected the right feed type?",
ErrorInvalidWordPressFeed: "The URL you provided does not appear to be a WordPress feed. Are you sure you selected the right feed type?",
AddToCalendarAriaLabel: "Press enter to download the calendar file to your device.",
AddToCalendarButtonLabel: "Add to my calendar",
AllDayDateFormat: "dddd, MMMM Do YYYY",
LocalizedTimeFormat: "llll",
FeedSettingsGroupName: "Calendar feed"
}
});

View File

@ -1,4 +1,12 @@
declare interface ICalendarFeedSummaryWebPartStrings {
UseCorsFieldDescription: string;
ConvertFromUTCFieldDescription: string;
ConvertFromUTCOptionNo: string;
ConvertFromUTCOptionYes: string;
ConvertFromUTCLabel: string;
MaxTotalFieldDescription: string;
MaxTotalFieldLabel: string;
FilmStripAriaLabel: string;
PropertyPaneDescription: string;
BasicGroupName: string;
AllItemsUrlFieldLabel: string;

View File

@ -1,5 +1,8 @@
{
"compilerOptions": {
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
@ -30,5 +33,6 @@
"exclude": [
"node_modules",
"lib"
]
],
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json"
}

View File

@ -43,12 +43,13 @@ import spservices from '../../services/spservices';
import { Map, ICoordinates, MapType } from "@pnp/spfx-controls-react/lib/Map";
import { EventRecurrenceInfo } from '../../controls/EventRecurrenceInfo/EventRecurrenceInfo';
import { getGUID } from '@pnp/common';
import { toLocaleShortDateString } from '../../utils/dateUtils';
const DayPickerStrings: IDatePickerStrings = {
months: [strings.January, strings.February, strings.March, strings.April, strings.May, strings.June, strings.July, strings.August, strings.September, strings.October, strings.November, strings.December],
shortMonths: [strings.Jan, strings.Feb, strings.Mar, strings.Apr, strings.May, strings.Jun, strings.Jul, strings.Aug, strings.Sep, strings.Oct, strings.Nov, strings.Dez],
days: [strings.Sunday, strings.Monday, strings.Tuesday, strings.Wednesday, strings.Thursday, strings.Friday, strings.Saturday],
shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Tursday, strings.ShortDay_Friday, strings.ShortDay_Saunday],
shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Thursday, strings.ShortDay_Friday, strings.ShortDay_Sunday],
goToToday: strings.GoToDay,
prevMonthAriaLabel: strings.PrevMonth,
nextMonthAriaLabel: strings.NextMonth,
@ -615,14 +616,14 @@ export class Event extends React.Component<IEventProps, IEventState> {
{
(this.state.eventData && (this.state.eventData.EventType !== "0" && this.state.showRecurrenceSeriesInfo !== true)) ?
<div>
<h2 style={{ display: 'inline-block', verticalAlign: 'top' }}>Recurrence Event</h2>
<h2 style={{ display: 'inline-block', verticalAlign: 'top' }}>{ strings.recurrenceEventLabel }</h2>
<DefaultButton
style={{ display: 'inline-block', marginLeft: '330px', verticalAlign: 'top', width: 'auto' }}
iconProps={{ iconName: 'RecurringEvent' }}
allowDisabledFocus={true}
onClick={this.onEditRecurrence}
>
Edit Recurrence Series
{ strings.editRecurrenceSeries }
</DefaultButton>
</div>
@ -658,6 +659,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
value={this.state.startDate}
label={strings.StartDateLabel}
onSelectDate={this.onSelectDateStart}
formatDate={toLocaleShortDateString}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
hidden={this.state.showRecurrenceSeriesInfo}
/>
@ -729,6 +731,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
value={this.state.endDate}
label={strings.EndDateLabel}
onSelectDate={this.onSelectDateEnd}
formatDate={toLocaleShortDateString}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
hidden={this.state.showRecurrenceSeriesInfo}
/>
@ -799,9 +802,9 @@ export class Event extends React.Component<IEventProps, IEventState> {
<Toggle
defaultChecked={false}
inlineLabel={true}
label="Recurrence ?"
onText="On"
offText="Off"
label={ strings.ifRecurrenceLabel }
onText={ strings.onLabel }
offText={ strings.offLabel }
onChange={(ev, checked: boolean) => {
ev.preventDefault();
this.setState({ showRecurrenceSeriesInfo: checked, newRecurrenceEvent: checked });
@ -827,7 +830,7 @@ export class Event extends React.Component<IEventProps, IEventState> {
)
}
< Label > Event Description</Label>
< Label > {strings.eventDescriptionLabel }</Label>
<div className={styles.description}>
<Editor

View File

@ -84,29 +84,29 @@ export class EventRecurrenceInfo extends React.Component<IEventRecurrenceInfoPro
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
<ChoiceGroup
label="Recurrence Information"
label={ strings.recurrenceInformationLabel }
selectedKey={this.state.selectedRecurrenceRule}
options={[
{
key: 'daily',
iconProps: { iconName: 'CalendarDay' },
text: 'Daily'
text: strings.dailyLabel
},
{
key: 'weekly',
iconProps: { iconName: 'CalendarWeek' },
text: 'Weekly'
text: strings.weeklyLabel
},
{
key: 'monthly',
iconProps: { iconName: 'Calendar' },
text: 'Monthly',
text: strings.monthlyLabel,
},
{
key: 'yearly',
iconProps: { iconName: 'Calendar' },
text: 'Yearly',
text: strings.yearlyLabel,
}
]}
onChange={this._onRecurrenceFrequenceChange}

View File

@ -13,6 +13,7 @@ import {
MaskedTextField,
} from 'office-ui-fabric-react';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import { toLocaleShortDateString } from '../../utils/dateUtils';
import spservices from '../../services/spservices';
@ -20,7 +21,7 @@ const DayPickerStrings: IDatePickerStrings = {
months: [strings.January, strings.February, strings.March, strings.April, strings.May, strings.June, strings.July, strings.August, strings.September, strings.October, strings.November, strings.December],
shortMonths: [strings.Jan, strings.Feb, strings.Mar, strings.Apr, strings.May, strings.Jun, strings.Jul, strings.Aug, strings.Sep, strings.Oct, strings.Nov, strings.Dez],
days: [strings.Sunday, strings.Monday, strings.Tuesday, strings.Wednesday, strings.Thursday, strings.Friday, strings.Saturday],
shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Tursday, strings.ShortDay_Friday, strings.ShortDay_Saunday],
shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Thursday, strings.ShortDay_Friday, strings.ShortDay_Sunday],
goToToday: strings.GoToDay,
prevMonthAriaLabel: strings.PrevMonth,
nextMonthAriaLabel: strings.NextMonth,
@ -44,7 +45,7 @@ export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceIn
super(props);
this.onPaternChange = this.onPaternChange.bind(this);
this.onPatternChange = this.onPatternChange.bind(this);
this.state = {
selectedKey: 'daily',
selectPatern: 'every',
@ -162,7 +163,7 @@ export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceIn
});
this.applyRecurrence();
}
private onPaternChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
private onPatternChange(ev: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void {
ev.preventDefault();
this.setState({
selectPatern: option.key,
@ -294,7 +295,7 @@ export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceIn
</div>
<div style={{ width: '100%', paddingTop: '10px' }}>
<Label>Patern</Label>
<Label>{ strings.patternLabel }</Label>
<ChoiceGroup
selectedKey={this.state.selectPatern}
options={[
@ -325,13 +326,13 @@ export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceIn
text: strings.everyweekdays,
}
]}
onChange={this.onPaternChange}
onChange={this.onPatternChange}
required={true}
/>
</div>
<div style={{ paddingTop: '22px' }}>
<Label>Date Range</Label>
<Label>{ strings.dateRangeLabel }</Label>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: '35px', paddingTop: '10px' }}>
<DatePicker
@ -342,6 +343,7 @@ export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceIn
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
formatDate={toLocaleShortDateString}
/>
</div>
@ -368,8 +370,10 @@ export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceIn
ariaLabel="Select a date"
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
formatDate={toLocaleShortDateString}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>
</div>
);
@ -390,7 +394,7 @@ export class EventRecurrenceInfoDaily extends React.Component<IEventRecurrenceIn
disabled={this.state.disableNumberOcurrences}
errorMessage={this.state.errorMessageNumberOcurrences}
onChange={this.onNumberOfOcurrencesChange} />
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', paddingLeft: '10px' } }}>Ocurrences</Label>
<Label styles={{ root: { display: 'inline-block', verticalAlign: 'top', paddingLeft: '10px' } }}>{ strings.occurrencesLabel }</Label>
</div>
);
}

View File

@ -16,6 +16,7 @@ import {
} from 'office-ui-fabric-react';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import { toLocaleShortDateString } from '../../utils/dateUtils';
import spservices from '../../services/spservices';
import { string } from 'prop-types';
@ -24,7 +25,7 @@ const DayPickerStrings: IDatePickerStrings = {
months: [strings.January, strings.February, strings.March, strings.April, strings.May, strings.June, strings.July, strings.August, strings.September, strings.October, strings.November, strings.December],
shortMonths: [strings.Jan, strings.Feb, strings.Mar, strings.Apr, strings.May, strings.Jun, strings.Jul, strings.Aug, strings.Sep, strings.Oct, strings.Nov, strings.Dez],
days: [strings.Sunday, strings.Monday, strings.Tuesday, strings.Wednesday, strings.Thursday, strings.Friday, strings.Saturday],
shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Tursday, strings.ShortDay_Friday, strings.ShortDay_Saunday],
shortDays: [strings.ShortDay_S, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Thursday, strings.ShortDay_Friday, strings.ShortDay_Sunday],
goToToday: strings.GoToDay,
prevMonthAriaLabel: strings.PrevMonth,
nextMonthAriaLabel: strings.NextMonth,
@ -580,6 +581,7 @@ export class EventRecurrenceInfoMonthly extends React.Component<IEventRecurrence
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
formatDate= {toLocaleShortDateString}
/>
</div>
@ -606,6 +608,7 @@ export class EventRecurrenceInfoMonthly extends React.Component<IEventRecurrence
ariaLabel={strings.StartDatePlaceHolder}
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
formatDate={toLocaleShortDateString}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>

View File

@ -14,24 +14,25 @@ import {
Checkbox,
} from 'office-ui-fabric-react';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import { toLocaleShortDateString } from '../../utils/dateUtils';
import spservices from '../../services/spservices';
const DayPickerStrings: IDatePickerStrings = {
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
months: [strings.January, strings.February, strings.March, strings.April, strings.May, strings.June, strings.July, strings.August, strings.September, strings.October, strings.November, strings.December],
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
shortMonths: [strings.Jan, strings.Feb, strings.Mar, strings.Apr, strings.May, strings.Jun, strings.Jul, strings.Aug, strings.Sep, strings.Oct, strings.Nov, strings.Dez],
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
days: [strings.Sunday, strings.Monday, strings.Tuesday, strings.Wednesday, strings.Thursday, strings.Friday, strings.Saturday],
shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
shortDays: [strings.ShortDay_Sunday, strings.ShortDay_M, strings.ShortDay_T, strings.ShortDay_W, strings.ShortDay_Thursday, strings.ShortDay_Friday, strings.ShortDay_S],
goToToday: 'Go to today',
prevMonthAriaLabel: 'Go to previous month',
nextMonthAriaLabel: 'Go to next month',
prevYearAriaLabel: 'Go to previous year',
nextYearAriaLabel: 'Go to next year',
closeButtonAriaLabel: 'Close date picker'
goToToday: strings.GoToDay,
prevMonthAriaLabel: strings.PrevMonth,
nextMonthAriaLabel: strings.NextMonth,
prevYearAriaLabel: strings.PrevYear,
nextYearAriaLabel: strings.NextYear,
closeButtonAriaLabel: strings.CloseDate
};
/**
@ -407,6 +408,7 @@ export class EventRecurrenceInfoWeekly extends React.Component<IEventRecurrenceI
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
formatDate={toLocaleShortDateString}
/>
</div>
@ -433,6 +435,7 @@ export class EventRecurrenceInfoWeekly extends React.Component<IEventRecurrenceI
ariaLabel={strings.StartDatePlaceHolder}
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
formatDate={toLocaleShortDateString}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>

View File

@ -15,7 +15,7 @@ import {
MaskedTextField
} from 'office-ui-fabric-react';
import { DatePicker, DayOfWeek, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import { toLocaleShortDateString } from '../../utils/dateUtils';
import spservices from '../../services/spservices';
const DayPickerStrings: IDatePickerStrings = {
@ -561,7 +561,7 @@ export class EventRecurrenceInfoYearly extends React.Component<IEventRecurrenceI
</div>
<div style={{ paddingTop: '22px' }}>
<Label>Date Range</Label>
<Label>{ strings.dateRangeLabel }</Label>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: '35px', paddingTop: '10px' }}>
<DatePicker
@ -572,6 +572,7 @@ export class EventRecurrenceInfoYearly extends React.Component<IEventRecurrenceI
label={strings.StartDateLabel}
value={this.state.startDate}
onSelectDate={this.onStartDateChange}
formatDate={toLocaleShortDateString}
/>
</div>
@ -598,6 +599,7 @@ export class EventRecurrenceInfoYearly extends React.Component<IEventRecurrenceI
ariaLabel={strings.StartDatePlaceHolder}
style={{ display: 'inline-block', verticalAlign: 'top', paddingLeft: '22px', }}
onSelectDate={this.onEndDateChange}
formatDate={toLocaleShortDateString}
value={this.state.endDate}
disabled={this.state.disableEndDate}
/>

View File

@ -0,0 +1,9 @@
import * as moment from 'moment';
export function toLocaleLongDateString(date: Date) {
return moment(date).format('LL');
}
export function toLocaleShortDateString(date: Date) {
return moment(date).format('ll');
}

View File

@ -62,23 +62,23 @@ define([], function () {
Tuesday: "Tuesday",
Monday: "Monday",
Sunday: "Sunday",
Jan:'Jan',
Feb:'Feb',
Mar:'Mar',
Apr:'Apr',
May:'May',
Jun:'Jun',
Jul:'Jul',
Aug:'Aug',
Sep:'Sep',
Oct:'Oct',
Nov:'Nov',
Dez:'Dez',
Jan:"Jan",
Feb:"Feb",
Mar:"Mar",
Apr:"Apr",
May:"May",
Jun:"Jun",
Jul:"Jul",
Aug:"Aug",
Sep:"Sep",
Oct:"Oct",
Nov:"Nov",
Dez:"Dez",
December: "December",
November: " 'November'",
November: "November",
October: "October",
September: "September",
August: " 'August'",
August: "August",
July: "July",
June: "June",
May: "May",
@ -100,17 +100,31 @@ define([], function () {
EventTitleErrorMessage: "Event Title is required.",
EventTitleLabel: "Event title",
EventPanelTitle: "Edit/Add Event",
"PropertyPaneDescription": "Calendar",
"BasicGroupName": "Properties",
SiteUrlFieldLabel: 'Site Url',
ListFieldLabel: 'Calendar List name',
weekLabel: 'Week',
dayLable: 'Day',
agenda: 'Agenda',
monthLabel: 'Month',
"todayLabel": 'Today',
"previousLabel": 'Previous',
"nextLabel": "Next",
"showMore": 'more'
PropertyPaneDescription: "Calendar",
BasicGroupName: "Properties",
SiteUrlFieldLabel: "Site Url",
ListFieldLabel: "Calendar List name",
weekLabel: "Week",
dayLable: "Day",
agenda: "Agenda",
monthLabel: "Month",
todayLabel: "Today",
previousLabel: "Previous",
nextLabel: "Next",
showMore: "more",
recurrenceEventLabel: "Recurrence Event",
editRecurrenceSeries: "Edit Recurrence Series",
ifRecurrenceLabel: "Recurrence ?",
onLabel: "On",
offLabel: "Off",
eventDescriptionLabel: "Event Description",
recurrenceInformationLabel: "Recurrence Information",
dailyLabel: "Daily",
weeklyLabel: "Weekly",
monthlyLabel: "Monthly",
yearlyLabel: "Yearly",
patternLabel: "Pattern",
dateRangeLabel: "Date Range",
occurrencesLabel: "occurrences"
}
});

View File

@ -47,9 +47,9 @@ declare interface ICalendarWebPartStrings {
NextMonth: string;
PrevMonth: string;
GoToDay: string;
ShortDay_Saunday: string;
ShortDay_Sunday: string;
ShortDay_Friday: string;
ShortDay_Tursday: string;
ShortDay_Thursday: string;
ShortDay_W: string;
ShortDay_T: string;
ShortDay_M: string;
@ -112,6 +112,21 @@ declare interface ICalendarWebPartStrings {
previousLabel: string;
nextLabel: string;
showMore: string;
recurrenceEventLabel: string;
editRecurrenceSeries: string;
ifRecurrenceLabel: string;
onLabel: string;
offLabel: string;
eventDescriptionLabel: string;
recurrenceInformationLabel: string;
dailyLabel: string;
weeklyLabel: string;
monthlyLabel: string;
yearlyLabel: string;
patternLabel: string;
dateRangeLabel: string;
occurrencesLabel: string;
}
declare module 'CalendarWebPartStrings' {

View File

@ -0,0 +1,131 @@
define([], function () {
return {
WeeksOnLabel: "vecka på",
PaternLabel: "Schema",
OcurrencesLabel: "Tillfällen",
dateRangeLabel: "Datumintervall",
weekEndDay: "Helgdag",
weekDayLabel: "Veckodag",
lastLabel: "sista",
fourthLabel: "fjärde",
thirdLabel: "tredje",
secondLabel: "andra",
firstLabel: "första",
theLabel: "",
MonthsLabel: "månad(er)",
ofEveryLabel: "för varje ",
AllowedValues1to12Label: "Tillåtna värder 1 till 12",
noEndDate: "inget slutdatum",
everyweekdays: "alla veckodagar",
days: "dag",
every: "var",
EndByLabel: "slutar den",
EndAfterLabel: "slutar efter",
HttpErrorMessage: "Fel vid inläsning av kalenderhändelser:",
CategoryPlaceHolder: "Välj en kategori",
CategoryLabel: "Kategori",
EnDateValidationMessage: "startdatum är senare än slutdatum",
SartDateValidationMessage: "startdatum är senare än slutdatum",
eventSelectDatesLabel: "Visa endast händelser mellan följande datum",
ConfirmeDeleteMessage: "Bekräfta borttag av händelse ? Om händelsen är en återkommande händelse kommer alla tillfällen att raderas ",
DialogConfirmDeleteTitle: "Ta bort händelse",
SpinnerDeletingLabel: "Tar bort...",
DialogCloseButtonLabel: "Avbryt",
DialogConfirmDeleteLabel: "Ta bort",
SaveButtonLabel: "Spara",
DeleteButtonLabel: "Ta bort",
CancelButtonLabel: "Avbryt",
LoadingEventsLabel: "Laddar händelser...",
WebPartConfigButtonLabel: "Inställningar",
WebpartConfigDescription: "Ställ in kalender lista ",
WebpartConfigIconText: "Ställ in Kalenderwebbdelen",
EventOwnerLabel: "ägare",
InvalidDateFormat: "Ogiltigt datumformat.",
IsRequired: "Fältet är obligatoriskt.",
CloseDate: "Stäng datumväljaren",
NextYear: "Gå till nästa år",
PrevYear: "Gå till föregående år",
NextMonth: "Gå till nästa månad",
PrevMonth: "Gå till föregående månad",
GoToDay: "Gå till idag",
ShortDay_Saunday: "S",
ShortDay_Friday: "F",
ShortDay_Tursday: "To",
ShortDay_W: "O",
ShortDay_T: "Ti",
ShortDay_M: "M",
ShortDay_S: "L",
Saturday: "Lördag",
Friday: "Fredag",
Thursday: "Torsdag",
Wednesday: "Onsdag",
Tuesday: "Tisdag",
Monday: "Måndag",
Sunday: "Söndag",
Jan:"Jan",
Feb:"Feb",
Mar:"Mar",
Apr:"Apr",
May:"Maj",
Jun:"Jun",
Jul:"Jul",
Aug:"Aug",
Sep:"Sep",
Oct:"Okt",
Nov:"Nov",
Dez:"Dec",
December: "December",
November: "November",
October: "Oktober",
September: "September",
August: "Augusti",
July: "Juli",
June: "Juni",
May: "Maj",
April: "April",
March: "Mars",
February: "Februari",
January: "Januari",
LocationLabel: "Platssökning och Karta",
LocationTextLabel: "Plats",
AttendeesLabel: "Deltagare",
EndMinLabel: "Min",
EndHourLabel: "Timme",
EndDateLabel: "Slutdatum",
EndDatePlaceHolder: "Välj ett datum...",
StartMinLabel: "Min",
StartHourLabel: "Timme",
StartDateLabel: "Startdatum",
StartDatePlaceHolder: "Välj ett datum...",
EventTitleErrorMessage: "Titel på händelsen är obligatoriskt.",
EventTitleLabel: "Titel",
EventPanelTitle: "Redigera/Lägg till händelse",
PropertyPaneDescription: "Kalender",
BasicGroupName: "Egenskaper",
SiteUrlFieldLabel: "Site Url",
ListFieldLabel: "Kalenderlista",
weekLabel: "Vecka",
dayLable: "Dag",
agenda: "Agenda",
monthLabel: "Månad",
todayLabel: "Idag",
previousLabel: "Föregående",
nextLabel: "Nästa",
showMore: "mer",
recurrenceEventLabel: "Återkommande händelse",
editRecurrenceSeries: "Redigera återkommande händelse",
ifRecurrenceLabel: "Återkommande ?",
onLabel: "På",
offLabel: "Av",
eventDescriptionLabel: "Beskrivning",
recurrenceInformationLabel: "Intervall",
dailyLabel: "Dagligen",
weeklyLabel: "Veckovis",
monthlyLabel: "Månatligen",
yearlyLabel: "Årligen",
patternLabel: "Schema",
dateRangeLabel: "Datumintervall",
occurrencesLabel: "tillfällen"
}
});

View File

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

32
samples/react-hooks/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": false,
"environment": "spo",
"version": "1.9.1",
"libraryName": "hellohooks",
"libraryId": "a31d666e-5429-4d34-8abe-2000c5627939",
"packageManager": "npm",
"isDomainIsolated": true,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,57 @@
# React Hooks Form WebPart
## Summary
The `React Hooks web part` is an example of how to implement Hooks in Spfx.
Hooks is a new feature included in React version 16.8, with the new version of **SharePoint Framework (SPFx) version 1.9.1** we can use them in our developments. In this example we are going to see the different types of hooks that are available and with the comparison of how this implementation can be done without the Hooks to be able to observe the benefits of using it.
![Brithdays Web Part](./assets/webpart.PNG)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.9.1-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
Existing list in tenant root site, with the Title "AvengersList" and columns:
Column Internal Name|Type|Required|Comments
--------------------|----|--------|----------
Title| Text| true
## Solution
Solution|Author(s)
--------|---------
react-hooks|Adrián Díaz [@AdrianDiaz81](https://www.twitter.com/adriandiaz81)
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|August 19, 2019|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Using React Hooks for building SharePoint Framework client-side web parts.
- Using @PnP/PnPjs to read items ...
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-hooks" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"hooks-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/hooks/HooksWebPart.js",
"manifest": "./src/webparts/hooks/HooksWebPart.manifest.json"
}
]
},
"classic-react-web-part-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/classicReactWebPart/ClassicReactWebPartWebPart.js",
"manifest": "./src/webparts/classicReactWebPart/ClassicReactWebPartWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"HooksWebPartStrings": "lib/webparts/hooks/loc/{locale}.js",
"ClassicReactWebPartWebPartStrings": "lib/webparts/classicReactWebPart/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "hellohooks-client-side-solution",
"id": "a31d666e-5429-4d34-8abe-2000c5627939",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": true
},
"paths": {
"zippedPackage": "solution/hellohooks.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 -->"
}

7
samples/react-hooks/gulpfile.js vendored Normal file
View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

17825
samples/react-hooks/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "hellohooks",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.9.1",
"@microsoft/sp-lodash-subset": "1.9.1",
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
"@microsoft/sp-webpart-base": "1.9.1",
"@pnp/common": "^1.3.4",
"@pnp/logging": "^1.3.4",
"@pnp/odata": "^1.3.4",
"@pnp/sp": "^1.3.4",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.9.1",
"@microsoft/sp-tslint-rules": "1.9.1",
"@microsoft/sp-module-interfaces": "1.9.1",
"@microsoft/sp-webpart-workbench": "1.9.1",
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "e4866e17-54f0-46b3-a695-59e356580e06",
"alias": "ClassicReactWebPartWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "ClassicReactWebPart" },
"description": { "default": "ClassicReactWebPart description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "ClassicReactWebPart"
}
}]
}

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { sp } from "@pnp/sp";
import * as strings from 'ClassicReactWebPartWebPartStrings';
import ClassicReactWebPart from './components/ClassicReactWebPart';
import { IClassicReactWebPartProps } from './components/IClassicReactWebPartProps';
export interface IClassicReactWebPartWebPartProps {
description: string;
}
export default class ClassicReactWebPartWebPart extends BaseClientSideWebPart<IClassicReactWebPartWebPartProps> {
public render(): void {
sp.setup({
spfxContext: this.context
});
const element: React.ReactElement<IClassicReactWebPartProps > = React.createElement(
ClassicReactWebPart,
{
description: this.properties.description
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,74 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.classicReactWebPart {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,12 @@
import * as React from 'react';
import { IClassicReactWebPartProps } from './IClassicReactWebPartProps';
import { LoadListComponent } from '../../hooks/components/LoadList';
export default class ClassicReactWebPart extends React.Component<IClassicReactWebPartProps, {}> {
public render(): React.ReactElement<IClassicReactWebPartProps> {
return (
<LoadListComponent />
);
}
}

View File

@ -0,0 +1,3 @@
export interface IClassicReactWebPartProps {
description: string;
}

View File

@ -0,0 +1,90 @@
import * as React from 'react';
import { sp } from "@pnp/sp";
import { Environment,EnvironmentType} from '@microsoft/sp-core-library';
export interface ILoadListComponentProps{
}
export interface ILoadListComponentState {
filter:string;
avengerCollection:any[];
}
export default class LoadListComponent extends React.Component<ILoadListComponentProps, ILoadListComponentState> {
public constructor(props: any) {
super(props);
this.setFilter=this.setFilter.bind(this);
this.state={
filter:'',
avengerCollection:[]
};
}
public loadAvengers (filter:string){
if (Environment.type==EnvironmentType.Local)
{
return fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`)
.then(response => response.json())
.then(json => this.setAvengerCollection(json));
}
else
{
if (filter=="")
{
sp.web.lists.getByTitle("AvengersList").items.select('Title', 'Id').get().then((items) => {
let result:any[]=[];
items.forEach(element => {result.push({"id":element["Id"], "name":element["Title"]
});
});
return result;
}).then(json => this.setAvengerCollection(json));;
}
else{
sp.web.lists.getByTitle("AvengersList").items.select('Title', 'Id').filter("substringof('" + filter + "',Title)")
.get().then((items) => {
let result:any[]=[];
items.forEach(element => {
result.push({
"id":element["Id"],
"name":element["Title"]
});
});
return result;
}).then(json => this.setAvengerCollection(json));
}
}
}
public componentWillMount(){
this.loadAvengers(this.state.filter);
}
public componentDidUpdate(prevProps, prevState) {
if (prevState.filter!==this.state.filter){
this.loadAvengers(prevState.filter);
}
}
private setAvengerCollection(json:any)
{
this.setState({
filter: json
});
}
private setFilter(event: any) {
this.setState({
filter: event.target.value
});
}
public render() {
return <div>
<input value={this.state.filter} onChange={this.setFilter} />
<ul>
{this.state.avengerCollection.map((user, index) => (
<li key={index}>{user.name}</li>
))}
</ul>
</div>;
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IClassicReactWebPartWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'ClassicReactWebPartWebPartStrings' {
const strings: IClassicReactWebPartWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "beedcf95-f6ec-4f3e-80e8-13f62ce792e6",
"alias": "HooksWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "hooks" },
"description": { "default": "hooks description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "hooks"
}
}]
}

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { sp } from "@pnp/sp";
import * as strings from 'HooksWebPartStrings';
import Hooks from './components/Hooks';
import { IHooksProps } from './components/IHooksProps';
export interface IHooksWebPartProps {
description: string;
}
export default class HooksWebPart extends BaseClientSideWebPart<IHooksWebPartProps> {
public render(): void {
sp.setup({
spfxContext: this.context
});
const element: React.ReactElement<IHooksProps > = React.createElement(
Hooks,
{
description: this.properties.description
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,47 @@
import * as React from 'react';
import { EnvironmentType} from '@microsoft/sp-core-library';
import { sp } from "@pnp/sp";
export const useAvengerCollection = (type:EnvironmentType) => {
const [filter, setFilter] = React.useState("");
const [avengerCollection, setAvengerCollection] = React.useState([]);
const loadAvengers = () => {
if (type==EnvironmentType.Local)
{
fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`)
.then(response => response.json())
.then(json => setAvengerCollection(json));
}
else
{
if (filter=="")
{
sp.web.lists.getByTitle("AvengersList").items.select('Title', 'Id').get().then((items) => {
let result:any[]=[];
items.forEach(element => {result.push({"id":element["Id"], "name":element["Title"]
});
});
return result;
})
.then(json => setAvengerCollection(json));
}
else{
sp.web.lists.getByTitle("AvengersList").items.select('Title', 'Id').filter("substringof('" + filter + "',Title)")
.get().then((items) => {
let result:any[]=[];
items.forEach(element => {
result.push({
"id":element["Id"],
"name":element["Title"]
});
});
return result;
})
.then(json => setAvengerCollection(json));
}
}
};
return {avengerCollection, loadAvengers, filter,setFilter, setAvengerCollection};
};

View File

@ -0,0 +1,74 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.hooks {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,10 @@
import * as React from 'react';
import { IHooksProps } from './IHooksProps';
import {LoadListComponent} from './LoadList';
export default class Hooks extends React.Component<IHooksProps, {}> {
public render(): React.ReactElement<IHooksProps> {
return (
<LoadListComponent />
);
}
}

View File

@ -0,0 +1,3 @@
export interface IHooksProps {
description: string;
}

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import {useAvengerCollection} from './AvengersCollection';
import { Environment} from '@microsoft/sp-core-library';
export const LoadListComponent = () => {
const {avengerCollection, loadAvengers, filter, setFilter} = useAvengerCollection(Environment.type);
React.useEffect(() => {
loadAvengers();
}, [filter]);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<ul>
{avengerCollection.map((user, index) => (
<li key={index}>{user.name}</li>
))}
</ul>
</div>
);
};

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IHooksWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'HooksWebPartStrings' {
const strings: IHooksWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -11,7 +11,7 @@ export default class BasicPlainSectionBackgroundExampleWebPart extends BaseClien
// See https://github.com/OfficeDev/office-ui-fabric-react/wiki/Theming
const semanticColors: Readonly<ISemanticColors> | undefined = this._themeVariant && this._themeVariant.semanticColors;
const style: string = ` style="color:${semanticColors.bodyText}"`;
const style: string = ` style="background-color:${semanticColors.bodyBackground}"`;
this.domElement.innerHTML = `<p${'' || (this._themeProvider && style)}>${this._textContent}</p>`;
}

View File

@ -11,7 +11,7 @@ export default class BasicSectionBackgroundExample extends React.Component<IBasi
const { semanticColors }: IReadonlyTheme = this.props.themeVariant;
return (
<div style={{color: semanticColors.bodyText}}>
<div style={{backgroundColor: semanticColors.bodyBackground}}>
<p>This React web part has support for section backgrounds and will inherit its background from the section</p>
</div>
);