Merge from 'dev' to 'master'.
Merge from 'dev' to 'master'.
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 3.8 MiB |
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export interface ICarouselContainerProps { }
|
||||
|
||||
export interface ICarouselContainerState { }
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./CarouselContainer";
|
||||
export * from "./CarouselContainer.types";
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -12,4 +12,4 @@ export interface IDateBoxState {
|
|||
export enum DateBoxSize {
|
||||
Small,
|
||||
Medium
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
// }
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.Paging {
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface IFilmstripLayoutProps {
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface IFilmstripLayoutState { }
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./FilmstripLayout";
|
||||
export * from "./FilmstripLayout.types";
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,4 +7,4 @@ export interface ICalendarEvent {
|
|||
category: string|undefined;
|
||||
description: string|undefined;
|
||||
location: string|undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ export interface ICalendarService {
|
|||
EventRange: CalendarEventRange;
|
||||
UseCORS: boolean;
|
||||
CacheDuration: number;
|
||||
MaxTotal: number;
|
||||
ConvertFromUTC: boolean;
|
||||
Name: string;
|
||||
getEvents: () => Promise<ICalendarEvent[]>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: []
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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');
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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' {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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" />
|
After Width: | Height: | Size: 219 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,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);
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IClassicReactWebPartProps {
|
||||
description: string;
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
declare interface IClassicReactWebPartWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'ClassicReactWebPartWebPartStrings' {
|
||||
const strings: IClassicReactWebPartWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IHooksProps {
|
||||
description: string;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
declare interface IHooksWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'HooksWebPartStrings' {
|
||||
const strings: IHooksWebPartStrings;
|
||||
export = strings;
|
||||
}
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|