Merge branch 'dev'

This commit is contained in:
VesaJuvonen 2019-05-04 18:07:52 +03:00
commit 4b8f5cd725
335 changed files with 92685 additions and 20157 deletions

View File

@ -1,11 +1,13 @@
# title of the sample
## Summary
Short summary on functionality and used technologies.
[picture of the web part in action]
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
@ -33,25 +35,27 @@ Version|Date|Comments
1.0|August 29, 2025|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`
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
> Include any additional steps as needed.
## Features
Description of the web part with possible additional details than in short summary.
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- topic 1
- topic 2
- topic 3
* topic 1
* topic 2
* topic 3
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />

View File

@ -57,4 +57,4 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
- Video Demonstration on using, building and code specifics for the sample web part:<br> http://warner.digital/dynamic-spfx-package-bundling/
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-dynamic-bundling-libraries" />

View File

@ -77,3 +77,4 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
* Logging.
* Rendering error messages.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-employee-spotlight" />

View File

@ -47,3 +47,5 @@ The _PowerBI Embedded_ Client-Side Web Part is built on the SharePoint Framework
All authentication and rendering happens client-side, there is no server-side component required.
It uses the [PowerBI Client](https://www.npmjs.com/package/powerbi-client) for rendering the PowerBI report.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-powerbi-embedded" />

View File

@ -63,3 +63,5 @@ gulp deploy-azure-storage
## Resources
[Handling Multiple Editions of SPFx Solution](http://tricky-sharepoint.blogspot.com/2017/08/handling-multiple-editions-of-spfx.html)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-solution-editions" />

View File

@ -67,6 +67,7 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
- Using the SharePoint Online REST API to manage Modern Experience Themes
## Additional Information:
- [Office UI Fabric Theme Palette Generator](https://developer.microsoft.com/en-us/fabric#/styles/themegenerator)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-theme-manager" />

View File

@ -45,3 +45,5 @@ Version|Date|Comments
* in the command line run:
* `npm install`
* `gulp serve`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-workbench-customizer" />

File diff suppressed because it is too large Load Diff

View File

@ -21,10 +21,8 @@
"officeFabricIconFontName": "DeveloperTools",
"properties": {
"requiresPageRefresh": false,
"maxWidth": true,
"centerCanvas": true,
"overflow": true,
"padding": true
"customWorkbenchStyles": true,
"previewMode": true
}
}]
}

View File

@ -5,16 +5,13 @@ import {
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import styles from './WorkbenchCustomizerWebPart.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';
import * as strings from 'WorkbenchCustomizerWebPartStrings';
export interface IWorkbenchCustomizerWebPartProps {
requiresPageRefresh: boolean;
maxWidth: boolean;
centerCanvas: boolean;
overflow: boolean;
padding: boolean;
customWorkbenchStyles: boolean;
previewMode: boolean;
}
export default class WorkbenchCustomizerWebPart extends BaseClientSideWebPart<IWorkbenchCustomizerWebPartProps> {
@ -26,30 +23,26 @@ export default class WorkbenchCustomizerWebPart extends BaseClientSideWebPart<IW
public async render(): Promise<void> {
if (this.properties.maxWidth) {
await import('./styles/maxWidth.module.scss');
}
if (this.properties.centerCanvas) {
await import('./styles/centerCanvas.module.scss');
}
if (this.properties.overflow) {
await import('./styles/overflow.module.scss');
}
if (this.properties.padding) {
await import('./styles/padding.module.scss');
}
if (!this.renderedOnce) {
this.domElement.innerHTML = `
if (this.properties.customWorkbenchStyles) {
await import('./styles/customWorkbenchStyles.module.scss');
}
if (this.properties.previewMode) {
const previewBtn = document.getElementsByName("Preview")[0];
previewBtn.click();
}
this.domElement.innerHTML = `
<div class="${styles.workbenchCustomizer}">
${this.properties.requiresPageRefresh
? `<div class="${styles.redMessage}">Please refresh the page to update workbench styles</div>`
: ''
}
<div>Max width enabled: ${this.properties.maxWidth}</div>
<div>Center canvas zone: ${this.properties.centerCanvas}</div>
<div>Custom overflow enabled: ${this.properties.overflow}</div>
<div>Custom padding enabled: ${this.properties.padding}</div>
? `<div class="${styles.redMessage}">Please refresh the page to remove custom workbench styles</div>`
: ''
}
*** Workbench Customizer web part ***
</div>`;
}
}
public onPropertyPaneFieldChanged(path: string, oldValue: any, newValue: any): void {
@ -75,17 +68,11 @@ export default class WorkbenchCustomizerWebPart extends BaseClientSideWebPart<IW
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneToggle('maxWidth', {
label: strings.MaxWidthFieldLabel
PropertyPaneToggle('customWorkbenchStyles', {
label: strings.CustomWorkbenchStylesFieldLabel
}),
PropertyPaneToggle('centerCanvas', {
label: strings.CenterCanvasFieldLabel
}),
PropertyPaneToggle('overflow', {
label: strings.OverflowFieldLabel
}),
PropertyPaneToggle('padding', {
label: strings.PaddingFieldLabel
PropertyPaneToggle('previewMode', {
label: strings.PreviewModeFieldLabel
})
]
}

View File

@ -2,9 +2,7 @@ define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Configuration",
"MaxWidthFieldLabel": "Enable custom max width",
"CenterCanvasFieldLabel": "Center canvas",
"OverflowFieldLabel": "Enable custom overflow",
"PaddingFieldLabel": "Enable custom padding"
"CustomWorkbenchStylesFieldLabel": "Enable custom styles for Workbench",
"PreviewModeFieldLabel": "Enable Preview mode by default",
}
});

View File

@ -1,10 +1,8 @@
declare interface IWorkbenchCustomizerWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
MaxWidthFieldLabel: string;
CenterCanvasFieldLabel: string;
OverflowFieldLabel: string;
PaddingFieldLabel: string;
CustomWorkbenchStylesFieldLabel: string;
PreviewModeFieldLabel: string;
}
declare module 'WorkbenchCustomizerWebPartStrings' {

View File

@ -1,9 +0,0 @@
:global #workbenchPageContent {
.CanvasComponent {
.CanvasZoneContainer {
.CanvasZone {
margin: auto;
}
}
}
}

View File

@ -1,6 +0,0 @@
:global #workbenchPageContent {
// max-width: 1316px;
max-width: 100%;
left: 0;
right: 0;
}

View File

@ -1,10 +0,0 @@
:global #workbenchPageContent {
&>div {
&>div {
overflow: visible;
&>div {
overflow: visible;
}
}
}
}

View File

@ -1,16 +0,0 @@
:global #workbenchPageContent {
&>div {
&>div {
&>div {
padding: 0;
.CanvasComponent {
.CanvasZoneContainer {
.CanvasZone {
padding-left: 24px;
}
}
}
}
}
}
}

View File

@ -52,4 +52,4 @@ This Web Part illustrates the following concepts on top of the SharePoint Framew
- styling components to match Fabric UI experience
- creating custom Property Pane fields (custom markup, logic) based on Knockout.js framework
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/ko-dependent-properties" />
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/knockout-dependent-properties" />

View File

@ -55,4 +55,4 @@ npm install
gulp serve --nobrowser
```
![](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/pnp-controls)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/pnp-controls" />

View File

@ -69,4 +69,5 @@ Once the npm packages are installed, run the command to preview your web parts i
```
gulp serve
```
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/tutorials" />
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/react-3rd-party-api" />

View File

@ -1,8 +0,0 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "react-relay-service",
"libraryId": "3d8b2b8b-cc29-4cf9-b3f1-62edd5ca9b52",
"environment": "spo"
}
}

View File

@ -1,26 +0,0 @@
## react-relay-service
This is where you include your WebPart documentation.
### Building the code
```bash
git clone the repo
npm i
npm i -g gulp
gulp
```
This package produces the following:
* lib/* - intermediate-stage commonjs build artifacts
* dist/* - the bundled script, along with other resources
* deploy/* - all resources which should be uploaded to a CDN.
### Build options
gulp clean - TODO
gulp test - TODO
gulp serve - TODO
gulp bundle - TODO
gulp package-solution - TODO

View File

@ -1,18 +0,0 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"hello-azure-relay-service-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/helloAzureRelayService/HelloAzureRelayServiceWebPart.js",
"manifest": "./src/webparts/helloAzureRelayService/HelloAzureRelayServiceWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"HelloAzureRelayServiceWebPartStrings": "lib/webparts/helloAzureRelayService/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -1,13 +0,0 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-relay-service-client-side-solution",
"id": "3d8b2b8b-cc29-4cf9-b3f1-62edd5ca9b52",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-relay-service.sppkg"
}
}

View File

@ -1,45 +0,0 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"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-case": true,
"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,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -1,4 +0,0 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -1,33 +0,0 @@
{
"name": "react-relay-service",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "15.6.2",
"react-dom": "15.6.2",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@types/webpack-env": ">=1.12.1 <1.14.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2"
}
}

View File

@ -1,26 +0,0 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "6ddbb470-dced-4bcb-ad30-2956dd91dce9",
"alias": "HelloAzureRelayServiceWebPart",
"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,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "HelloAzureRelayService" },
"description": { "default": "HelloAzureRelayService description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "HelloAzureRelayService"
}
}]
}

View File

@ -1,78 +0,0 @@
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 * as strings from 'HelloAzureRelayServiceWebPartStrings';
import HelloAzureRelayService from './components/HelloAzureRelayService';
import { IHelloAzureRelayServiceProps } from './components/IHelloAzureRelayServiceProps';
import { HttpClient, IHttpClientOptions, HttpClientResponse } from '@microsoft/sp-http';
export interface IHelloAzureRelayServiceWebPartProps {
description: string;
}
export default class HelloAzureRelayServiceWebPart extends BaseClientSideWebPart<IHelloAzureRelayServiceWebPartProps> {
private results: Array<any>;
public onInit(): Promise<any> {
debugger;
const requestHeaders: Headers = new Headers();
requestHeaders.append('Content-type', 'application/json');
requestHeaders.append('Cache-Control', 'no-cache');
const httpClientOptions: IHttpClientOptions = {
headers: requestHeaders
};
const url = "https://relayserviceproxy20180831034031.azurewebsites.net/api/Document?webID=917E82F1-FDF8-4298-AE73-1DD60E244C67";
return this.context.httpClient.get(url, HttpClient.configurations.v1, httpClientOptions).then((response) => {
response.json().then((r => {
debugger;
this.results = r;
}))
}).catch((err) => {
console.log(err);
})
}
public render(): void {
const element: React.ReactElement<IHelloAzureRelayServiceProps> = React.createElement(
HelloAzureRelayService,
{
description: this.properties.description,
documents: this.results
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -1,32 +0,0 @@
import * as React from 'react';
import styles from './HelloAzureRelayService.module.scss';
import { IHelloAzureRelayServiceProps } from './IHelloAzureRelayServiceProps';
import { escape } from '@microsoft/sp-lodash-subset';
export default class HelloAzureRelayService extends React.Component<IHelloAzureRelayServiceProps, {}> {
public render(): React.ReactElement<IHelloAzureRelayServiceProps> {
return (
<div className={styles.helloAzureRelayService}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<span className={styles.title}>Welcome to SharePoint!</span>
<p className={styles.subTitle}>Customize SharePoint experiences using Web Parts.</p>
<p className={styles.description}>{escape(this.props.description)}</p>
<a href="https://aka.ms/spfx" className={styles.button}>
<ul>
{this.props.documents.map((d, i) => {
debugger;
return <li>{d.Title}</li>;
})}
</ul>
</a>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -1,4 +0,0 @@
export interface IHelloAzureRelayServiceProps {
description: string;
documents:Array<any>;
}

View File

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

View File

@ -63,10 +63,3 @@ This sample illustrates the following concepts on top of the SharePoint Framewor
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-accordion" />
### Build options
gulp clean - TODO
gulp test - TODO
gulp serve - TODO
gulp bundle - TODO
gulp package-solution - TODO

View File

@ -79,3 +79,4 @@ This sample web part shows how adaptive cards can be used effectively with Share
[figure4]: ./assets/list-sample-data.png
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-adaptive-cards-image-gallery" />

View File

@ -75,7 +75,6 @@ This Web Part displays the events from multiple calendars located in various sit
### Building the code
```bash
@ -98,3 +97,5 @@ gulp test
gulp serve
gulp bundle
gulp package-solution
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-aggregated-calendar" />

View File

@ -29,10 +29,13 @@ Existing list in tenant root site, with the Title "Birthdays" and columns:
Column Internal Name|Type|Required| comments
--------------------|----|--------|----------
jobTitle| Text| no|
JobTitle| Text| no|
Birthday| DateTime | true|
userAADGUID| Text| no | required if used Azure Function to get Birthdays from AAD
Title| Text| true
email| Text| true
## After create a column Index on column "Brithday" - Important!
## Solution
@ -51,7 +54,7 @@ Version|Date|Comments
---
## Minimal Path to Awesome
## Minimal Path to Awesome - please follow all the steps.
- Clone this repository
- in the command line run:
@ -59,6 +62,8 @@ Version|Date|Comments
- `gulp build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- `Add and Deploy Package to AppCatalog `
- `Go to API Management - from SharePoint Admin Center new experience, and Approve the Permission Require to Use Graph API SCOPES`
@ -73,4 +78,5 @@ This sample illustrates the following concepts on top of the SharePoint Framewor
- using @PnP/PnPjs to create a List, add, update, delete Items.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-birthdays" />

View File

@ -6,8 +6,24 @@
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "User.ReadBasic.All"
},
{
"resource": "Microsoft Graph",
"scope": "Sites.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "profile"
}]
},
"paths": {
"zippedPackage": "solution/birdthays.sppkg"

View File

@ -37,7 +37,7 @@ export class SPService {
}
}
this.graphClient = await this._context.msGraphClientFactory.getClient();
_results = await this.graphClient.api(`sites/root/lists('${this.birthdayListTitle}')/items`)
_results = await this.graphClient.api(`sites/root/lists('${this.birthdayListTitle}')/items?orderby=Fields/Birthday`)
.version('v1.0')
.expand('fields')
.top(upcommingDays)

View File

@ -0,0 +1,10 @@
{
"extends": "stylelint-config-standard",
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true
}
}

View File

@ -0,0 +1,32 @@
{
"@pnp/generator-spfx": {
"framework": "react",
"pnpFramework": "reactjs.plus",
"pnp-libraries": [
"jquery@3",
"@pnp/pnpjs",
"@pnp/spfx-property-controls",
"@pnp/spfx-controls-react"
],
"pnp-ci": [
"azure"
],
"pnp-vetting": [
"webpack-analyzer",
"stylelint"
],
"spfxenv": "spo"
},
"@microsoft/generator-sharepoint": {
"environment": "spo",
"framework": "react",
"plusBeta": true,
"isCreatingSolution": true,
"version": "1.8.0",
"libraryName": "react-calendar",
"libraryId": "3a13208b-3874-4036-9262-4edd22e88187",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,125 @@
# React Calendar
## Summary
This Web Part allows you to manage events in a calendar.
Uses a list of existing calendars on any website.
The location and name of the list and the dates of the events to be displayed are defined in the properties of the web part.
The Events are created in Site TimeZone, defined in site Regional Settings.
Each category has its own color that is generated in the load.
The Web Part checks the user's permissions for the View, Add, Edit, and Delete events.
The Web Part does not show recurring events, I will work on it soon.
##
![callendar](/samples/react-calendar/assets/animatevideo.gif)
## Web Part - Screenshots
![callendar](/samples/react-calendar//assets/screen1.png)
![callendar](/samples/react-calendar/assets/screen1.0.jpg)
![callendar](/samples/react-calendar/assets/screen1.1.png)
![callendar](/samples/react-calendar/assets/screen1.2.png)
![callendar](/samples/react-calendar/assets/screen1.3.png)
![callendar](/samples/react-calendar//assets/screen1.4.png)
![callendar](/samples/react-calendar//assets/screen2.png)
![callendar](/samples/react-calendar/assets/screen3.png)
![callendar](/samples/react-calendar//assets/screen4.png)
![callendar](/samples/react-calendar/assets/screen5.png)
![callendar](/samples/react-calendar//assets/screen6.png)
![callendar](/samples/react-calendar//assets/screen7.png)
![callendar](/samples/react-calendar/assets/screen8.png)
![callendar](/samples/react-calendar//assets/screen9.png)
##
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-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)
> Update accordingly as needed.
## WebPart Properties
Property |Type|Required| comments
--------------------|----|--------|----------
Site Url of Calendar List | Text| yes|
Calendar list| Text| yes| this is filled with all list of type "event list" created
Start Date | Date | yes | Event Date
End Date| Date| yes | Event Date
## Solution
The Web Part Use PnPjs library, Office-ui-fabric-react components. react Big-Calendar Compoment
Solution|Author(s)
--------|---------
Calendar Web Part|João Mendes
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|April 25, 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 build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- `Add to AppCatalog and deploy`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

View File

@ -0,0 +1,71 @@
resources:
- repo: self
trigger:
- master
- develop
queue:
name: Hosted VS2017
demands:
- npm
- node.js
steps:
#install node 8.x
- task: NodeTool@0
displayName: 'Use Node 8.x'
inputs:
versionSpec: 8.x
checkLatest: true
#install nodejs modules with npm
- task: Npm@1
displayName: 'npm install'
inputs:
workingDir: '$(Build.SourcesDirectory)'
verbose: false
#start unit tests
- task: Gulp@0
displayName: 'gulp test'
inputs:
gulpFile: '$(Build.SourcesDirectory)/gulpfile.js'
targets: test
publishJUnitResults: true
testResultsFiles: '**/test-*.xml'
#publish test results
- task: PublishCodeCoverageResults@1
displayName: 'Publish Code Coverage Results $(Build.SourcesDirectory)/temp/coverage/cobertura/cobertura.xml'
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(Build.SourcesDirectory)/temp/coverage/cobertura/cobertura.xml'
reportDirectory: '$(Build.SourcesDirectory)/temp/coverage/cobertura'
#bundle code with gulp
- task: Gulp@0
displayName: 'gulp bundle'
inputs:
gulpFile: '$(Build.SourcesDirectory)/gulpfile.js'
targets: bundle
arguments: '--ship'
continueOnError: true
#package solution with gulp
- task: Gulp@0
displayName: 'gulp package-solution'
inputs:
gulpFile: '$(Build.SourcesDirectory)/gulpfile.js'
targets: 'package-solution'
arguments: '--ship'
#copy files to artifact repository
- task: CopyFiles@2
displayName: 'Copy Files to: $(build.artifactstagingdirectory)/drop'
inputs:
Contents: '**\*.sppkg'
TargetFolder: '$(build.artifactstagingdirectory)/drop'
#publish artifacts
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/drop'

View File

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

View File

@ -2,6 +2,6 @@
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-related-items",
"container": "react-calendar",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,4 @@
{
"preset": "@voitanos/jest-preset-spfx-react16",
"rootDir": "../src"
}

View File

@ -0,0 +1,30 @@
"use strict";
var existingKarmaConfig = require('@microsoft/sp-build-web/lib/karma/karma.config');
var junitReporter = require('karma-junit-reporter');
module.exports = function (config) {
existingKarmaConfig(config);
config.reporters.push('junit');
config.set({
basePath: './..',
});
config.junitReporter = {
outputDir: 'temp/', // results will be saved as $outputDir/$browserName.xml
outputFile: 'test-results.xml', // if included, results will be saved as $outputDir/$browserName/$outputFile
suite: 'karma', // suite will become the package name attribute in xml testsuite element
useBrowserName: true, // add browser name to report and classes names
};
var coberturaSubDir = 'cobertura';
var coverageSubDir = 'lcov';
var coberturaFileName = 'cobertura.xml';
config.coverageReporter.reporters.push({type: 'cobertura', subdir: './' + coberturaSubDir, file: coberturaFileName});
config.coverageReporter.reporters.push({
type: 'lcov',
subdir: './' + coverageSubDir + '/',
file: 'lcov.info'
});
config.browserNoActivityTimeout = 60000;
config.plugins.push(junitReporter);
};

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-calendar-client-side-solution",
"id": "3a13208b-3874-4036-9262-4edd22e88187",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-calendar.sppkg"
}
}

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

93
samples/react-calendar/gulpfile.js vendored Normal file
View File

@ -0,0 +1,93 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
// Create clean development package
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Webpack Bundle Anlayzer
* Reference and gulp task
*/
const bundleAnalyzer = require('webpack-bundle-analyzer');
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
const lastDirName = path.basename(__dirname);
const dropPath = path.join(__dirname, 'temp', 'stats');
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
generateStatsFile: true,
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
logLevel: 'error'
}));
return generatedConfiguration;
}
});
/**
* StyleLinter configuration
* Reference and custom gulp task
*/
const stylelint = require('gulp-stylelint');
/* Stylelinter sub task */
let styleLintSubTask = build.subTask('stylelint', (gulp) => {
console.log('[stylelint]: By default style lint errors will not break your build. If you want to change this behaviour, modify failAfterError parameter in gulpfile.js.');
return gulp
.src('src/**/*.scss')
.pipe(stylelint({
failAfterError: false,
reporters: [{
formatter: 'string',
console: true
}]
}));
});
/* end sub task */
build.rig.addPreBuildTask(styleLintSubTask);
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);
/**
* Continuous Integration
*/
const buildConfig = build.getConfig();
const karmaTaskCandidates = buildConfig.uniqueTasks.filter(task => task.name === 'karma');
if (karmaTaskCandidates && karmaTaskCandidates.length > 0) {
const karmaTask = karmaTaskCandidates[0];
karmaTask.taskConfig.configPath = './config/karma.config.js';
}

20664
samples/react-calendar/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
{
"name": "react-calendar",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"preversion": "node ./tools/pre-version.js",
"postversion": "gulp dist",
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
},
"dependencies": {
"@microsoft/rush-stack-compiler-3.2": "^0.3.6",
"@microsoft/sp-core-library": "1.8.0-plusbeta",
"@microsoft/sp-lodash-subset": "1.8.0-plusbeta",
"@microsoft/sp-office-ui-fabric-core": "1.8.0-plusbeta",
"@microsoft/sp-property-pane": "1.8.0-plusbeta",
"@microsoft/sp-webpart-base": "1.8.0-plusbeta",
"@pnp/pnpjs": "^1.3.0",
"@pnp/spfx-controls-react": "1.12.0",
"@pnp/spfx-property-controls": "1.14.1",
"@types/draft-js": "^0.10.30",
"@types/es6-promise": "0.0.33",
"@types/globalize": "0.0.34",
"@types/jquery": "^3.3.29",
"@types/react": "16.7.22",
"@types/react-big-calendar": "^0.20.13",
"@types/react-dom": "16.0.5",
"@types/webpack-env": "1.13.1",
"draft-js": "^0.10.5",
"draftjs-to-html": "^0.8.4",
"globalize": "^1.4.2",
"immutable": "^4.0.0-rc.12",
"jquery": "^3.3.1",
"moment": "^2.24.0",
"moment-timezone": "^0.5.25",
"react": "16.7.0",
"react-big-calendar": "^0.20.4",
"react-dom": "16.7.0",
"react-draft-wysiwyg": "^1.13.2",
"typescript": "^3.2.4"
},
"resolutions": {
"@types/react": "16.4.2"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-2.7": "0.4.0",
"@microsoft/sp-build-web": "1.8.0-plusbeta",
"@microsoft/sp-module-interfaces": "1.8.0-plusbeta",
"@microsoft/sp-tslint-rules": "1.8.0-plusbeta",
"@microsoft/sp-webpart-workbench": "1.8.0-plusbeta",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@voitanos/jest-preset-spfx-react16": "^1.1.0",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "1.0.0",
"gulp-stylelint": "^8.0.0",
"jest": "^23.6.0",
"karma-junit-reporter": "^1.2.0",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0",
"stylelint-scss": "^3.5.4",
"webpack-bundle-analyzer": "^3.1.0"
}
}

View File

@ -0,0 +1,98 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.description {
border: 1px solid #a6a6a6;
padding: 10;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
padding-top: 10px;
height: 300px;
max-height: 320px;
overflow: auto;
}
.description:hover {
border-color: rgb( 51, 51, 51 );
}
.calendar {
.container {
max-width: 100%;
min-height: 400px;
height: 600px;
margin: 0px auto;
}
.eventTitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
border-style: solid;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,14 @@
import { IEventData } from '../../services/IEventData';
import { IPanelModelEnum} from './IPanelModeEnum';
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IEventProps {
event: IEventData;
panelMode: IPanelModelEnum;
onDissmissPanel: (refresh:boolean) => void;
showPanel: boolean;
startDate?: Date;
endDate?: Date;
context:WebPartContext;
siteUrl: string;
listId:string;
}

View File

@ -0,0 +1,28 @@
import { IEventData } from '../../services/IEventData';
import { IUserPermissions } from '../../services/IUserPermissions';
import { DayOfWeek} from 'office-ui-fabric-react/lib/DatePicker';
import { IDropdownOption } from 'office-ui-fabric-react/';
export interface IEventState {
showPanel: boolean;
eventData:IEventData;
firstDayOfWeek?: DayOfWeek;
startSelectedHour: IDropdownOption ;
startSelectedMin: IDropdownOption ;
endSelectedHour: IDropdownOption ;
endSelectedMin: IDropdownOption ;
startDate?: Date;
endDate?: Date;
editorState?: any;
selectedUsers: string[];
locationLatitude: number;
locationLongitude: number;
errorMessage?:string;
hasError?:boolean;
disableButton?: boolean;
isSaving?:boolean;
isDeleting?:boolean;
displayDialog:boolean;
userPermissions?: IUserPermissions;
isloading:boolean;
siteRegionalSettings: any;
}

View File

@ -0,0 +1,5 @@
export enum IPanelModelEnum {
add=1,
edit=2,
delete=3
}

View File

@ -0,0 +1,769 @@
import * as React from 'react';
import styles from './Event.module.scss';
import * as strings from 'CalendarWebPartStrings';
import { IEventProps } from './IEventProps';
import { IEventState } from './IEventState';
import { escape } from '@microsoft/sp-lodash-subset';
import * as moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { PeoplePicker, PrincipalType } from "@pnp/spfx-controls-react/lib/PeoplePicker";
import {
Panel,
PanelType,
TextField,
Label,
extendComponent
} from 'office-ui-fabric-react';
import { EnvironmentType } from '@microsoft/sp-core-library';
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import { IEventData } from '../../services/IEventData';
import { IUserPermissions } from '../../services/IUserPermissions';
import {
DatePicker,
DayOfWeek,
IDatePickerStrings,
Dropdown,
DropdownMenuItemType,
IDropdownStyles,
IDropdownOption,
DefaultButton,
PrimaryButton,
IPersonaProps,
MessageBar,
MessageBarType,
Spinner,
SpinnerSize,
Dialog,
DialogType,
DialogFooter,
Toggle
}
from 'office-ui-fabric-react';
import { addMonths, addYears } from 'office-ui-fabric-react/lib/utilities/dateMath/DateMath';
import { _ComponentBaseKillSwitches } from '@microsoft/sp-component-base';
import { IPanelModelEnum } from './IPanelModeEnum';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import spservices from '../../services/spservices';
import { Map, ICoordinates, MapType } from "@pnp/spfx-controls-react/lib/Map";
const today: Date = new Date(Date.now());
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.Dezember],
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],
goToToday: strings.GoToDay,
prevMonthAriaLabel: strings.PrevMonth,
nextMonthAriaLabel: strings.NextMonth,
prevYearAriaLabel: strings.PrevYear,
nextYearAriaLabel: strings.NextYear,
closeButtonAriaLabel: strings.CloseDate,
isRequiredErrorMessage: strings.IsRequired,
invalidInputErrorMessage: strings.InvalidDateFormat,
};
export class Event extends React.Component<IEventProps, IEventState> {
private spService: spservices = null;
private attendees: IPersonaProps[] = [];
private latitude: number = 41.1931819;
private longitude: number = -8.4897452;
private categoryDropdownOption: IDropdownOption[] = [];
public constructor(props) {
super(props);
/* geolocation is available */
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition((position) => {
this.latitude = position.coords.latitude;
this.longitude = position.coords.longitude;
});
} else {
/* geolocation IS NOT available */
console.log('browser Geolocation is not available');
}
// Initialize Map coordinates
console.log('ini', this.latitude, this.longitude);
this.state = {
showPanel: false,
eventData: this.props.event,
startSelectedHour: { key: '09', text: '00' },
startSelectedMin: { key: '00', text: '00' },
endSelectedHour: { key: '18', text: '00' },
endSelectedMin: { key: '00', text: '00' },
editorState: EditorState.createEmpty(),
selectedUsers: [],
locationLatitude: this.latitude,
locationLongitude: this.longitude,
hasError: false,
errorMessage: '',
disableButton: true,
isSaving: false,
displayDialog: false,
isloading: false,
siteRegionalSettings: undefined,
userPermissions: { hasPermissionAdd: false, hasPermissionDelete: false, hasPermissionEdit: false, hasPermissionView: false },
};
// local copia of props
this.onStartChangeHour = this.onStartChangeHour.bind(this);
this.onStartChangeMin = this.onStartChangeMin.bind(this);
this.onEndChangeHour = this.onEndChangeHour.bind(this);
this.onEndChangeMin = this.onEndChangeMin.bind(this);
this.onEditorStateChange = this.onEditorStateChange.bind(this);
this.onRenderFooterContent = this.onRenderFooterContent.bind(this);
this.onSave = this.onSave.bind(this);
this.onSelectDateEnd = this.onSelectDateEnd.bind(this);
this.onSelectDateStart = this.onSelectDateStart.bind(this);
this.onUpdateCoordinates = this.onUpdateCoordinates.bind(this);
this.onGetErrorMessageTitle = this.onGetErrorMessageTitle.bind(this);
this.getPeoplePickerItems = this.getPeoplePickerItems.bind(this);
this.hidePanel = this.hidePanel.bind(this);
this.onDelete = this.onDelete.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.confirmDelete = this.confirmDelete.bind(this);
this.onAllDayEventChange = this.onAllDayEventChange.bind(this);
this.onCategoryChanged = this.onCategoryChanged.bind(this);
this.spService = new spservices(this.props.context);
}
/**
* Hide Panel
*
* @private
* @memberof Event
*/
private hidePanel() {
this.props.onDissmissPanel(false);
}
/**
* Save Event to a list
* @private
* @memberof Event
*/
private async onSave() {
let eventData: IEventData = this.state.eventData;
// All Day event ?
const startDate = `${moment(this.state.startDate).format('YYYY/MM/DD')}`;
const startTime = `${this.state.startSelectedHour.key}:${this.state.startSelectedMin.key}`;
const startDateTime = `${startDate} ${startTime}`;
const start = moment(startDateTime, 'YYYY/MM/DD HH:mm').toLocaleString();
eventData.start = new Date(start);
// End Date
const endDate = `${moment(this.state.endDate).format('YYYY/MM/DD')}`;
const endTime = `${this.state.endSelectedHour.key}:${this.state.endSelectedMin.key}`;
const endDateTime = `${endDate} ${endTime}`;
const end = moment(endDateTime, 'YYYY/MM/DD HH:mm').toLocaleString();
eventData.end = new Date(end);
debugger;
// get Geolocation
eventData.geolocation = { Latitude: this.latitude, Longitude: this.longitude };
const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude);
eventData.location = locationInfo ? locationInfo.display_name : 'N/A';
console.log('beforeupd',eventData.geolocation);
// get Attendees
if (!eventData.attendes) { //vinitialize if no attendees
eventData.attendes = [];
}
// Get Descript from RichText Compoment
eventData.Description = draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()));
try {
for (const user of this.attendees) {
const userInfo: any= await this.spService.getUserByLoginName(user.id, this.props.siteUrl);
eventData.attendes.push(parseInt(userInfo.Id));
}
this.setState({ isSaving: true });
switch (this.props.panelMode) {
case IPanelModelEnum.edit:
await this.spService.updateEvent(eventData, this.props.siteUrl, this.props.listId);
break;
case IPanelModelEnum.add:
await this.spService.addEvent(eventData, this.props.siteUrl, this.props.listId);
break;
default:
break;
}
this.setState({ isSaving: false });
this.props.onDissmissPanel(true);
} catch (error) {
this.setState({ hasError: true, errorMessage: error.message, isSaving: false });
}
}
/**
*
* @param {*} error
* @param {*} errorInfo
* @memberof Event
*/
public componentDidCatch(error: any, errorInfo: any) {
this.setState({ hasError: true, errorMessage: errorInfo.componentStack });
}
/**
*
*
* @memberof Event
*/
public async componentDidMount() {
this.setState({ isloading: true });
let editorState:EditorState;
// Load Regional Settings
const siteRegionalSettigns = await this.spService.getSiteRegionalSettingsTimeZone(this.props.siteUrl);
// chaeck User list Permissions
const userListPermissions: IUserPermissions = await this.spService.getUserPermissions(this.props.siteUrl, this.props.listId);
// Load Categories
this.categoryDropdownOption = await this.spService.getChoiceFieldOptions(this.props.siteUrl, this.props.listId, 'Category');
// Edit Mode ?
if (this.props.panelMode == IPanelModelEnum.edit && this.props.event) {
// Get hours of event
const startHour = moment(this.props.event.start).format('HH').toString();
const startMin = moment(this.props.event.start).format('mm').toString();
const endHour = moment(this.props.event.end).format('HH').toString();
const endMin = moment(this.props.event.end).format('mm').toString();
// Get Descript and covert to RichText Control
const html = this.props.event.Description;
const contentBlock = htmlToDraft(html);
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
editorState = EditorState.createWithContent(contentState);
}
// testa attendees
const attendees = this.props.event.attendes;
let selectedUsers: string[] = [];
if (attendees && attendees.length > 0) {
for (const userId of attendees) {
let user: any = await this.spService.getUserById(userId, this.props.siteUrl);
if (user) {
selectedUsers.push(user.UserPrincipalName);
}
}
}
// Has geolocation ?
this.latitude = this.props.event.geolocation && this.props.event.geolocation.Latitude ? this.props.event.geolocation.Latitude : this.latitude;
this.longitude = this.props.event.geolocation && this.props.event.geolocation.Longitude ? this.props.event.geolocation.Longitude : this.longitude;
// Update Component Data
this.setState({
eventData: this.props.event,
startDate: this.props.event.start,
endDate: this.props.event.end,
startSelectedHour: { key: startHour, text: startHour },
startSelectedMin: { key: startMin, text: startMin },
endSelectedHour: { key: endHour, text: endHour },
endSelectedMin: { key: endMin, text: endMin },
editorState: editorState,
selectedUsers: selectedUsers,
userPermissions: userListPermissions,
isloading: false,
siteRegionalSettings: siteRegionalSettigns,
locationLatitude: this.latitude,
locationLongitude: this.longitude,
});
} else {
editorState = EditorState.createEmpty();
this.setState({
startDate: this.props.startDate ? this.props.startDate : new Date(),
endDate: this.props.endDate ? this.props.endDate : new Date(),
editorState: editorState,
userPermissions: userListPermissions,
isloading: false,
siteRegionalSettings: siteRegionalSettigns,
});
}
}
/**
*
* @memberof Event
*/
public componentWillMount() {
}
/**
* @private
* @memberof Event
*/
private onStartChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ startSelectedHour: item });
}
/**
* @private
* @memberof Event
*/
private onEndChangeHour = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ endSelectedHour: item });
}
/**
* @private
* @memberof Event
*/
private onStartChangeMin = (ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
ev.preventDefault();
this.setState({ startSelectedMin: item });
}
/**
* @private
* @param {any[]} items
* @memberof Event
*/
private getPeoplePickerItems(items: any[]) {
this.attendees = [];
this.attendees = items;
}
/**
*
* @private
* @param {*} editorState
* @memberof Event
*/
private onEditorStateChange(editorState) {
this.setState({
editorState,
});
}
/**
*
* @private
* @param {string} value
* @returns {string}
* @memberof Event
*/
private onGetErrorMessageTitle(value: string): string {
let returnMessage: string = '';
if (value.length === 0) {
returnMessage = strings.EventTitleErrorMessage;
} else {
this.setState({ eventData: { ...this.state.eventData, title: value }, disableButton: false, errorMessage: '' });
}
return returnMessage;
}
/**
*
* @private
* @memberof Event
*/
private onEndChangeMin(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
ev.preventDefault();
this.setState({ endSelectedMin: item });
}
/**
*
*
* @private
* @param {React.FormEvent<HTMLDivElement>} ev
* @param {IDropdownOption} item
* @memberof Event
*/
private onCategoryChanged(ev: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void {
ev.preventDefault();
this.setState({ eventData: { ...this.state.eventData, Category: item.text } });
}
/**
*
* @private
* @param {React.MouseEvent<HTMLDivElement>} event
* @memberof Event
*/
private onDelete(ev: React.MouseEvent<HTMLDivElement>) {
ev.preventDefault();
this.setState({ displayDialog: true });
}
/**
*
* @private
* @param {React.MouseEvent<HTMLDivElement>} event
* @memberof Event
*/
private closeDialog(ev: React.MouseEvent<HTMLDivElement>) {
ev.preventDefault();
this.setState({ displayDialog: false });
}
private async confirmDelete(ev: React.MouseEvent<HTMLDivElement>) {
ev.preventDefault();
try {
this.setState({ isDeleting: true });
switch (this.props.panelMode) {
case IPanelModelEnum.edit:
await this.spService.deleteEvent(this.state.eventData, this.props.siteUrl, this.props.listId);
break;
default:
break;
}
this.setState({ isDeleting: false });
this.props.onDissmissPanel(true);
} catch (error) {
this.setState({ hasError: true, errorMessage: error.message, isDeleting: false });
}
}
/**
* @private
* @returns
* @memberof Event
*/
private onRenderFooterContent() {
return (
<div >
<DefaultButton onClick={this.hidePanel} style={{ marginBottom: '15px', float: 'right' }}>
{strings.CancelButtonLabel}
</DefaultButton>
{
this.props.panelMode == IPanelModelEnum.edit && this.state.userPermissions.hasPermissionDelete && (
<DefaultButton onClick={this.onDelete} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}>
{strings.DeleteButtonLabel}
</DefaultButton>
)
}
{
(this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit) &&
<PrimaryButton disabled={this.state.disableButton} onClick={this.onSave} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }}>
{strings.SaveButtonLabel}
</PrimaryButton>
}
{
this.state.isSaving &&
<Spinner size={SpinnerSize.medium} style={{ marginBottom: '15px', marginRight: '8px', float: 'right' }} />
}
</div>
);
}
/**
*
* @private
* @param {Date} newDate
* @memberof Event
*/
private onSelectDateStart(newDate: Date) {
this.setState({ startDate: newDate });
}
/**
* @private
* @param {Date} newDate
* @memberof Event
*/
private onSelectDateEnd(newDate: Date) {
this.setState({ endDate: newDate });
}
private onAllDayEventChange(ev: React.MouseEvent<HTMLElement>, checked: boolean) {
ev.preventDefault();
this.setState({ eventData: { ...this.state.eventData, allDayEvent: checked } });
}
/**
*
* @private
* @param {ICoordinates} coordinates
* @memberof Event
*/
private async onUpdateCoordinates(coordinates: ICoordinates) {
this.latitude = coordinates.latitude;
this.longitude = coordinates.longitude;
console.log('upcoor',this.latitude + ' ' + this.longitude);
const locationInfo = await this.spService.getGeoLactionName(this.latitude, this.longitude);
this.setState({ eventData: { ...this.state.eventData, location: locationInfo.display_name } });
}
public render(): React.ReactElement<IEventProps> {
console.log(this.state.locationLatitude + '-' + this.state.locationLongitude);
const { editorState } = this.state;
return (
<div>
<Panel
isOpen={this.props.showPanel}
onDismiss={this.hidePanel}
type={PanelType.medium}
headerText={strings.EventPanelTitle}
isFooterAtBottom={true}
onRenderFooterContent={this.onRenderFooterContent}
>
<div style={{ width: '100%' }}>
{
this.state.hasError &&
<MessageBar messageBarType={MessageBarType.error}>
{this.state.errorMessage}
</MessageBar>
}
{
this.state.isloading && (
<Spinner size={SpinnerSize.large} />
)
}
{
!this.state.isloading &&
<div>
<div>
<TextField
label={strings.EventTitleLabel}
value={this.state.eventData ? this.state.eventData.title : ''}
onGetErrorMessage={this.onGetErrorMessageTitle}
deferredValidationTime={500}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div>
<Dropdown
label={strings.CategoryLabel}
selectedKey={this.state.eventData && this.state.eventData.Category ? this.state.eventData.Category : ''}
onChange={this.onCategoryChanged}
options={this.categoryDropdownOption}
placeholder={strings.CategoryPlaceHolder}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<DatePicker
isRequired={false}
strings={DayPickerStrings}
placeholder={strings.StartDatePlaceHolder}
ariaLabel={strings.StartDatePlaceHolder}
allowTextInput={true}
value={this.state.startDate}
label={strings.StartDateLabel}
onSelectDate={this.onSelectDateStart}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<Dropdown
selectedKey={this.state.startSelectedHour.key}
onChange={this.onStartChangeHour}
label={strings.StartHourLabel}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '01', text: '01' },
{ key: '02', text: '02' },
{ key: '03', text: '03' },
{ key: '04', text: '04' },
{ key: '05', text: '05' },
{ key: '06', text: '06' },
{ key: '07', text: '07' },
{ key: '08', text: '08' },
{ key: '09', text: '09' },
{ key: '10', text: '10' },
{ key: '11', text: '11' },
{ key: '12', text: '12' },
{ key: '13', text: '13' },
{ key: '14', text: '14' },
{ key: '15', text: '15' },
{ key: '16', text: '16' },
{ key: '17', text: '17' },
{ key: '18', text: '18' },
{ key: '19', text: '19' },
{ key: '20', text: '20' },
{ key: '21', text: '21' },
{ key: '22', text: '22' },
{ key: '23', text: '23' }
]}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', }}>
<Dropdown
label={strings.StartMinLabel}
selectedKey={this.state.startSelectedMin.key}
onChange={this.onStartChangeMin}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '05', text: '05' },
{ key: '10', text: '10' },
{ key: '15', text: '15' },
{ key: '20', text: '20' },
{ key: '25', text: '25' },
{ key: '30', text: '30' },
{ key: '35', text: '35' },
{ key: '40', text: '40' },
{ key: '45', text: '45' },
{ key: '50', text: '50' },
{ key: '55', text: '55' }
]}
/>
</div>
<br />
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<DatePicker
isRequired={false}
strings={DayPickerStrings}
placeholder={strings.EndDatePlaceHolder}
ariaLabel={strings.EndDatePlaceHolder}
allowTextInput={true}
value={this.state.endDate}
label={strings.EndDateLabel}
onSelectDate={this.onSelectDateEnd}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', paddingRight: 10 }}>
<Dropdown
selectedKey={this.state.endSelectedHour.key}
onChange={this.onEndChangeHour}
label={strings.EndHourLabel}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '01', text: '01' },
{ key: '02', text: '02' },
{ key: '03', text: '03' },
{ key: '04', text: '04' },
{ key: '05', text: '05' },
{ key: '06', text: '06' },
{ key: '07', text: '07' },
{ key: '08', text: '08' },
{ key: '09', text: '09' },
{ key: '10', text: '10' },
{ key: '11', text: '11' },
{ key: '12', text: '12' },
{ key: '13', text: '13' },
{ key: '14', text: '14' },
{ key: '15', text: '15' },
{ key: '16', text: '16' },
{ key: '17', text: '17' },
{ key: '18', text: '18' },
{ key: '19', text: '19' },
{ key: '20', text: '20' },
{ key: '21', text: '21' },
{ key: '22', text: '22' },
{ key: '23', text: '23' }
]}
/>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top', }}>
<Dropdown
label={strings.EndMinLabel}
selectedKey={this.state.endSelectedMin.key}
onChange={this.onEndChangeMin}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
options={[
{ key: '00', text: '00' },
{ key: '05', text: '05' },
{ key: '10', text: '10' },
{ key: '15', text: '15' },
{ key: '20', text: '20' },
{ key: '25', text: '25' },
{ key: '30', text: '30' },
{ key: '35', text: '35' },
{ key: '40', text: '40' },
{ key: '45', text: '45' },
{ key: '50', text: '50' },
{ key: '55', text: '55' },
{ key: '59', text: '59' }
]}
/>
</div>
<Label>{this.state.siteRegionalSettings ? this.state.siteRegionalSettings.Description : ''}</Label>
<br />
<Label>Event Description</Label>
<div className={styles.description}>
<Editor
editorState={editorState}
onEditorStateChange={this.onEditorStateChange}
ReadOnly={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div>
<PeoplePicker
webAbsoluteUrl={this.props.siteUrl}
context={this.props.context}
titleText={strings.AttendeesLabel}
principalTypes={[PrincipalType.User]}
resolveDelay={1000}
showtooltip={true}
selectedItems={this.getPeoplePickerItems}
personSelectionLimit={10}
defaultSelectedUsers={this.state.selectedUsers}
disabled={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? false : true}
/>
</div>
<div>
<TextField
value={this.state.eventData && this.state.eventData.location ? this.state.eventData.location : ''}
label={strings.LocationTextLabel}
readOnly
multiline />
</div>
<div>
<Map titleText={strings.LocationLabel}
coordinates={{ latitude: this.state.locationLatitude, longitude: this.state.locationLongitude }}
enableSearch={this.state.userPermissions.hasPermissionAdd || this.state.userPermissions.hasPermissionEdit ? true : false}
onUpdateCoordinates={this.onUpdateCoordinates}
/>
</div>
</div>
}
</div>
{
this.state.displayDialog &&
<div>
<Dialog
hidden={!this.state.displayDialog}
dialogContentProps={{
type: DialogType.normal,
title: strings.DialogConfirmDeleteTitle,
showCloseButton: false
}}
modalProps={{
isBlocking: true,
styles: { main: { maxWidth: 450 } }
}}
>
<Label >{strings.ConfirmeDeleteMessage}</Label>
{
this.state.isDeleting &&
<Spinner size={SpinnerSize.medium} ariaLabel={strings.SpinnerDeletingLabel} />
}
<DialogFooter>
<PrimaryButton onClick={this.confirmDelete} text={strings.DialogConfirmDeleteLabel} disabled={this.state.isDeleting} />
<DefaultButton onClick={this.closeDialog} text={strings.DialogCloseButtonLabel} />
</DialogFooter>
</Dialog>
</div>
}
</Panel>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
export interface IEventData {
id?:number;
title: string;
Description?: any;
location?:string;
start: Date;
end: Date;
color?:string;
ownerInitial?: string;
ownerPhoto?:string;
ownerEmail?:string;
ownerName?:string;
allDayEvent?: boolean;
attendes?: number[];
geolocation?: {Longitude:number, Latitude: number};
Category?: string;
}

View File

@ -0,0 +1,4 @@
export interface IList {
ID: string;
Title: string;
}

View File

@ -0,0 +1,5 @@
export interface IListFields {
Title: String;
InternalName: string;
TypeAsString: string;
}

View File

@ -0,0 +1,6 @@
export interface IUserPermissions {
hasPermissionAdd: boolean;
hasPermissionEdit: boolean;
hasPermissionDelete: boolean;
hasPermissionView: boolean;
}

View File

@ -0,0 +1,470 @@
// João Mendes
// March 2019
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { sp, Fields, Web, SearchResults, Field, PermissionKind, RegionalSettings } from '@pnp/sp';
import { graph, } from "@pnp/graph";
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions, HttpClient, MSGraphClient } from '@microsoft/sp-http';
import * as $ from 'jquery';
import { IEventData } from './IEventData';
import { registerDefaultFontFaces } from "@uifabric/styling";
import { EventArgs } from "@microsoft/sp-core-library";
import * as moment from 'moment';
import { SiteUser } from "@pnp/sp/src/siteusers";
import { IUserPermissions } from './IUserPermissions';
import { dateAdd } from "@pnp/common";
const ADMIN_ROLETEMPLATE_ID = "62e90394-69f5-4237-9190-012177145e10"; // Global Admin TemplateRoleId
// Class Services
export default class spservices {
private graphClient: MSGraphClient = null;
constructor(private context: WebPartContext) {
// Setuo Context to PnPjs and MSGraph
sp.setup({
spfxContext: this.context
});
graph.setup({
spfxContext: this.context
});
// Init
this.onInit();
}
// OnInit Function
private async onInit() {
//this.appCatalogUrl = await this.getAppCatalogUrl();
}
/**
*
* @private
* @param {string} siteUrl
* @returns {Promise<number>}
* @memberof spservices
*/
private async getSiteTimeZoneHoursToUtc(siteUrl: string): Promise<number> {
let numberHours: number = 0;
let siteTimeZoneHoursToUTC: any;
let siteTimeZoneBias: number;
let siteTimeZoneDaylightBias: number;
let currentDateTimeOffSet: number = new Date().getTimezoneOffset() / 60;
try {
const siteRegionalSettings: any = await this.getSiteRegionalSettingsTimeZone(siteUrl);
// Calculate hour to current site
siteTimeZoneBias = siteRegionalSettings.Information.Bias;
siteTimeZoneDaylightBias = siteRegionalSettings.Information.DaylightBias;
// Formula to calculate the number of hours need to get UTC Date.
numberHours = (siteTimeZoneBias / 60) + (siteTimeZoneDaylightBias / 60) - currentDateTimeOffSet;
}
catch (error) {
return Promise.reject(error);
}
return numberHours;
}
/**
*
* @param {IEventData} newEvent
* @param {string} siteUrl
* @param {string} listId
* @returns
* @memberof spservices
*/
public async addEvent(newEvent: IEventData, siteUrl: string, listId: string) {
let results = null;
try {
const web = new Web(siteUrl);
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.add({
Title: newEvent.title,
Description: newEvent.Description,
Geolocation: newEvent.geolocation,
ParticipantsPickerId: { results: newEvent.attendes },
EventDate: new Date(moment(newEvent.start).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
EndDate: new Date(moment(newEvent.end).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
Location: newEvent.location,
fAllDayEvent: false,
fRecurrence: false,
Category: newEvent.Category,
});
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {IEventData} newEvent
* @param {string} siteUrl
* @param {string} listId
* @returns
* @memberof spservices
*/
public async updateEvent(updateEvent: IEventData, siteUrl: string, listId: string) {
let results = null;
try {
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl);
const web = new Web(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.getById(updateEvent.id).update({
Title: updateEvent.title,
Description: updateEvent.Description,
Geolocation: updateEvent.geolocation,
ParticipantsPickerId: { results: updateEvent.attendes },
EventDate: new Date(moment(updateEvent.start).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
EndDate: new Date(moment(updateEvent.end).add(siteTimeZoneHoursToUTC, 'hours').toISOString()),
Location: updateEvent.location,
fAllDayEvent: false,
fRecurrence: false,
Category: updateEvent.Category,
});
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {IEventData} event
* @param {string} siteUrl
* @param {string} listId
* @returns
* @memberof spservices
*/
public async deleteEvent(event: IEventData, siteUrl: string, listId: string) {
let results = null;
try {
const web = new Web(siteUrl);
//"Title","fRecurrence", "fAllDayEvent","EventDate", "EndDate", "Description","ID", "Location","Geolocation","ParticipantsPickerId"
results = await web.lists.getById(listId).items.getById(event.id).delete();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {number} userId
* @param {string} siteUrl
* @returns {Promise<SiteUser>}
* @memberof spservices
*/
public async getUserById(userId: number, siteUrl: string): Promise<SiteUser> {
let results: SiteUser = null;
if (!userId && !siteUrl) {
return null;
}
try {
const web = new Web(siteUrl);
results = await web.siteUsers.getById(userId).get();
//results = await web.siteUsers.getByLoginName(userId).get();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
*
* @param {string} loginName
* @param {string} siteUrl
* @returns {Promise<SiteUser>}
* @memberof spservices
*/
public async getUserByLoginName(loginName: string, siteUrl: string): Promise<SiteUser> {
let results: SiteUser = null;
if (!loginName && !siteUrl) {
return null;
}
try {
const web = new Web(siteUrl);
await web.ensureUser(loginName);
results = await web.siteUsers.getByLoginName(loginName).get();
//results = await web.siteUsers.getByLoginName(userId).get();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @param {string} loginName
* @returns
* @memberof spservices
*/
public async getUserProfilePictureUrl(loginName: string) {
let results: any = null;
try {
results = await sp.profiles.getPropertiesFor(loginName);
} catch (error) {
results = null;
}
return results.PictureUrl;
}
/**
*
* @param {string} siteUrl
* @param {string} listId
* @returns {Promise<IUserPermissions>}
* @memberof spservices
*/
public async getUserPermissions(siteUrl: string, listId: string): Promise<IUserPermissions> {
let hasPermissionAdd: boolean = false;
let hasPermissionEdit: boolean = false;
let hasPermissionDelete: boolean = false;
let hasPermissionView: boolean = false;
let userPermissions: IUserPermissions = undefined;
try {
const web = new Web(siteUrl);
hasPermissionAdd = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.AddListItems);
hasPermissionEdit = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.EditListItems);
hasPermissionDelete = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.DeleteListItems);
hasPermissionView = await web.lists.getById(listId).currentUserHasPermissions(PermissionKind.ViewListItems);
userPermissions = { hasPermissionAdd: hasPermissionAdd, hasPermissionEdit: hasPermissionEdit, hasPermissionDelete: hasPermissionDelete, hasPermissionView: hasPermissionView };
} catch (error) {
return Promise.reject(error);
}
return userPermissions;
}
/**
*
* @param {string} siteUrl
* @returns
* @memberof spservices
*/
public async getSiteLists(siteUrl: string) {
let results: any[] = [];
if (!siteUrl) {
return [];
}
try {
const web = new Web(siteUrl);
results = await web.lists.select("Title", "ID").filter('BaseTemplate eq 106').get();
} catch (error) {
return Promise.reject(error);
}
return results;
}
/**
*
* @private
* @returns
* @memberof spservices
*/
public async colorGenerate() {
var hexValues = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e"];
var newColor = "#";
for (var i = 0; i < 6; i++) {
var x = Math.round(Math.random() * 14);
var y = hexValues[x];
newColor += y;
}
return newColor;
}
/**
*
* @param {string} siteUrl
* @param {string} listId
* @param {string} fieldInternalName
* @returns {Promise<{ key: string, text: string }[]>}
* @memberof spservices
*/
public async getChoiceFieldOptions(siteUrl: string, listId: string, fieldInternalName: string): Promise<{ key: string, text: string }[]> {
let fieldOptions: { key: string, text: string }[] = [];
try {
const web = new Web(siteUrl);
const results = await web.lists.getById(listId)
.fields
.getByInternalNameOrTitle(fieldInternalName)
.select("Title", "InternalName", "Choices")
.get();
if (results && results.Choices.length > 0) {
for (const option of results.Choices) {
fieldOptions.push({
key: option,
text: option
});
}
}
} catch (error) {
return Promise.reject(error);
}
return fieldOptions;
}
/**
*
* @param {string} siteUrl
* @param {string} listId
* @param {Date} eventStartDate
* @param {Date} eventEndDate
* @returns {Promise< IEventData[]>}
* @memberof spservices
*/
public async getEvents(siteUrl: string, listId: string, eventStartDate: Date, eventEndDate: Date): Promise<IEventData[]> {
let events: IEventData[] = [];
if (!siteUrl) {
return [];
}
try {
// Get Regional Settings TimeZone Hours to UTC
const siteTimeZoneHoursToUTC: number = await this.getSiteTimeZoneHoursToUtc(siteUrl);
// Get Category Field Choices
const categoryDropdownOption = await this.getChoiceFieldOptions(siteUrl, listId, 'Category');
let categoryColor: { category: string, color: string }[] = [];
for (const cat of categoryDropdownOption) {
categoryColor.push({ category: cat.text, color: await this.colorGenerate() });
}
const web = new Web(siteUrl);
const results = await web.lists.getById(listId).renderListDataAsStream(
{
DatesInUtc: true,
ViewXml: `<View><ViewFields><FieldRef Name='Author'/><FieldRef Name='Category'/><FieldRef Name='Description'/><FieldRef Name='ParticipantsPicker'/><FieldRef Name='Geolocation'/><FieldRef Name='ID'/><FieldRef Name='EndDate'/><FieldRef Name='EventDate'/><FieldRef Name='ID'/><FieldRef Name='Location'/><FieldRef Name='Title'/><FieldRef Name='fAllDayEvent'/></ViewFields>
<Query>
<Where>
<And>
<And>
<Geq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventStartDate).format('YYYY-MM-DD')}</Value>
</Geq>
<Leq>
<FieldRef Name='EventDate' />
<Value IncludeTimeValue='false' Type='DateTime'>${moment(eventEndDate).format('YYYY-MM-DD')}</Value>
</Leq>
</And>
<Eq>
<FieldRef Name='fRecurrence' />
<Value Type='Recurrence'>0</Value>
</Eq>
</And>
</Where>
</Query>
<RowLimit Paged=\"FALSE\">2000</RowLimit>
</View>`
}
);
if (results && results.Row.length > 0) {
for (const event of results.Row) {
const initialsArray: string[] = event.Author[0].title.split(' ');
const initials: string = initialsArray[0].charAt(0) + initialsArray[initialsArray.length - 1].charAt(0);
const userPictureUrl = await this.getUserProfilePictureUrl(`i:0#.f|membership|${event.Author[0].email}`);
const attendees: number[] = [];
const first: number = event.Geolocation.indexOf('(') + 1;
const last: number = event.Geolocation.indexOf(')');
const geo = event.Geolocation.substring(first, last);
const geolocation = geo.split(' ');
const CategoryColorValue: any[] = categoryColor.filter((value) => {
return value.category == event.Category;
});
for (const attendee of event.ParticipantsPicker) {
attendees.push(parseInt(attendee.id));
}
events.push({
id: event.ID,
title: event.Title,
Description: event.Description,
// start: moment(event.EventDate).utc().toDate().setUTCMinutes(this.siteTimeZoneOffSet),
start: new Date(moment(event.EventDate).subtract(siteTimeZoneHoursToUTC, 'hour').toISOString()),
// end: new Date(moment(event.EndDate).toLocaleString()),
end: new Date(moment(event.EndDate).subtract(siteTimeZoneHoursToUTC, 'hour').toISOString()),
location: event.Location,
ownerEmail: event.Author[0].email,
ownerPhoto: userPictureUrl ?
`https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${event.Author[0].email}&UA=0&size=HR96x96` : '',
ownerInitial: initials,
// color: await this.colorGenerate(),
color: CategoryColorValue.length > 0 ? CategoryColorValue[0].color : await this.colorGenerate,
ownerName: event.Author[0].title,
attendes: attendees,
allDayEvent: false,
geolocation: { Longitude: parseFloat(geolocation[0]), Latitude: parseFloat(geolocation[1]) },
Category: event.Category
});
}
}
// Return Data
return events;
} catch (error) {
console.dir(error);
return Promise.reject(error);
}
}
/**
*
* @private
* @param {string} siteUrl
* @returns
* @memberof spservices
*/
public async getSiteRegionalSettingsTimeZone(siteUrl: string) {
let regionalSettings: RegionalSettings;
try {
const web = new Web(siteUrl);
regionalSettings = await web.regionalSettings.timeZone.get();
} catch (error) {
return Promise.reject(error);
}
return regionalSettings;
}
/**
* @param {string} webUrl
* @param {string} siteDesignId
* @returns
* @memberof spservices
*/
public async getGeoLactionName(latitude: number, longitude: number) {
try {
const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`;
const results = await $.ajax({
url: apiUrl,
type: 'GET',
dataType: 'json',
headers: {
'content-type': 'application/json;charset=utf-8',
'accept': 'application/json;odata=nometadata',
}
});
if (results) {
return results;
}
} catch (error) {
return Promise.reject(error);
}
}
}

View File

@ -0,0 +1,39 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "24353da0-cf7a-4ca8-85a2-c9cc91b5b865",
"alias": "CalendarWebPart",
"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",
"TeamsTab"
],
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": {
"default": "Other"
},
"title": {
"default": "Calendar"
},
"description": {
"default": "Calendar Events"
},
"officeFabricIconFontName": "Calendar",
"properties": {
"title": "Calendar",
"siteUrl": "",
"list": "",
"eventStartDate": "",
"eventEndDate": ""
}
}
]
}

View File

@ -0,0 +1,294 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart, PropertyPaneHorizontalRule } from '@microsoft/sp-webpart-base';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
PropertyPaneLabel
} from '@microsoft/sp-property-pane';
import * as strings from 'CalendarWebPartStrings';
import Calendar from './components/Calendar';
import { ICalendarProps } from './components/ICalendarProps';
import { PropertyFieldDateTimePicker, DateConvention, TimeConvention, IDateTimeFieldValue } from '@pnp/spfx-property-controls/lib/PropertyFieldDateTimePicker';
export interface ICalendarWebPartProps {
title: string;
siteUrl: string;
list: string;
eventStartDate: IDateTimeFieldValue ;
eventEndDate: IDateTimeFieldValue;
errorMessage: string;
}
import spservices from '../../services/spservices';
import * as moment from 'moment';
import { format } from '@uifabric/utilities';
export default class CalendarWebPart extends BaseClientSideWebPart<ICalendarWebPartProps> {
private lists: IPropertyPaneDropdownOption[] = [];
private listsDropdownDisabled: boolean = true;
private spService: spservices = null;
private errorMessage: string;
public constructor() {
super();
}
public render(): void {
const element: React.ReactElement<ICalendarProps> = React.createElement(
Calendar,
{
title: this.properties.title,
siteUrl: this.properties.siteUrl,
list: this.properties.list,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.title = value;
},
context: this.context,
eventStartDate: this.properties.eventStartDate,
eventEndDate: this.properties.eventEndDate,
}
);
ReactDom.render(element, this.domElement);
}
// onInit
public async onInit(): Promise<void> {
this.spService = new spservices(this.context);
this.properties.siteUrl = this.context.pageContext.site.absoluteUrl;
if (!this.properties.eventStartDate){
this.properties.eventStartDate = { value: moment().subtract(2,'years').startOf('month').toDate(), displayValue: moment().format('ddd MMM MM YYYY')};
}
if (!this.properties.eventEndDate){
this.properties.eventEndDate = { value: moment().add(20,'years').endOf('month').toDate(), displayValue: moment().format('ddd MMM MM YYYY')};
}
if (this.properties.siteUrl && !this.properties.list) {
const _lists = await this.loadLists();
this.lists = _lists;
this.properties.list = this.lists.length > 0 ? this.lists[0].key.toString() : '';
}
return Promise.resolve();
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
/**
*
* @protected
* @memberof CalendarWebPart
*/
protected async onPropertyPaneConfigurationStart() {
try {
if (this.properties.siteUrl) {
const _lists = await this.loadLists();
this.lists = _lists;
this.listsDropdownDisabled = false;
// await this.loadFields(this.properties.siteUrl);
this.context.propertyPane.refresh();
} else {
this.lists = [];
this.properties.list = '';
this.listsDropdownDisabled = false;
this.context.propertyPane.refresh();
}
} catch (error) {
}
}
/**
*
* @private
* @returns {Promise<IPropertyPaneDropdownOption[]>}
* @memberof CalendarWebPart
*/
private async loadLists(): Promise<IPropertyPaneDropdownOption[]> {
const _lists: IPropertyPaneDropdownOption[] = [];
try {
const results = await this.spService.getSiteLists(this.properties.siteUrl);
for (const list of results) {
_lists.push({ key: list.Id, text: list.Title });
}
} catch (error) {
this.errorMessage = `${error.message} - ${strings.PropPanelSiteUrlErrorMessage}` ;
this.context.propertyPane.refresh();
}
return _lists;
}
/**
*
* @private
* @param {string} date
* @returns
* @memberof CalendarWebPart
*/
private onEventStartDateValidation(date:string){
if (date && this.properties.eventEndDate.value){
if (moment(date).isAfter(moment(this.properties.eventEndDate.value))){
return strings.SartDateValidationMessage;
}
}
return '';
}
/**
*
* @private
* @param {string} date
* @returns
* @memberof CalendarWebPart
*/
private onEventEndDateValidation(date:string){
if (date && this.properties.eventEndDate.value){
if (moment(date).isBefore( moment(this.properties.eventStartDate.value))){
return strings.EnDateValidationMessage;
}
}
return '';
}
/**
*
* @private
* @param {string} value
* @returns {Promise<string>}
* @memberof CalendarWebPart
*/
private onSiteUrlGetErrorMessage(value: string) {
let returnValue: string = '';
if (value) {
returnValue = '';
} else {
const previousList: string = this.properties.list;
const previousSiteUrl: string = this.properties.siteUrl;
// reset selected item
this.properties.list = undefined;
this.properties.siteUrl = undefined;
this.lists = [];
this.listsDropdownDisabled = true;
this.onPropertyPaneFieldChanged('list', previousList, this.properties.list);
this.onPropertyPaneFieldChanged('siteUrl', previousSiteUrl, this.properties.siteUrl);
this.context.propertyPane.refresh();
}
return returnValue;
}
/**
*
* @protected
* @param {string} propertyPath
* @param {string} oldValue
* @param {string} newValue
* @memberof CalendarWebPart
*/
protected async onPropertyPaneFieldChanged(propertyPath: string, oldValue: string, newValue: string) {
try {
// reset any error
this.properties.errorMessage = undefined;
this.errorMessage = undefined;
this.context.propertyPane.refresh();
if (propertyPath === 'siteUrl' && newValue) {
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
const _oldValue = this.properties.list;
this.onPropertyPaneFieldChanged('list', _oldValue, this.properties.list);
this.context.propertyPane.refresh();
const _lists = await this.loadLists();
this.lists = _lists;
this.listsDropdownDisabled = false;
this.properties.list = this.lists.length > 0 ? this.lists[0].key.toString() : undefined;
this.context.propertyPane.refresh();
this.render();
}
else {
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
} catch (error) {
this.errorMessage = `${error.message} - ${strings.PropPanelSiteUrlErrorMessage}` ;
this.context.propertyPane.refresh();
}
}
/**
*
* @protected
* @returns {IPropertyPaneConfiguration}
* @memberof CalendarWebPart
*/
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('siteUrl', {
label: strings.SiteUrlFieldLabel,
onGetErrorMessage: this.onSiteUrlGetErrorMessage.bind(this),
value: this.context.pageContext.site.absoluteUrl,
deferredValidationTime: 1200,
}),
PropertyPaneDropdown('list', {
label: strings.ListFieldLabel,
options: this.lists,
disabled: this.listsDropdownDisabled,
}),
PropertyPaneLabel('eventStartDate', {
text: strings.eventSelectDatesLabel
}),
PropertyFieldDateTimePicker('eventStartDate', {
label: 'From',
initialDate: this.properties.eventStartDate,
dateConvention: DateConvention.Date,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
onGetErrorMessage: this.onEventStartDateValidation,
deferredValidationTime: 0,
key: 'eventStartDateId'
}),
PropertyFieldDateTimePicker('eventEndDate', {
label: 'to',
initialDate: this.properties.eventEndDate,
dateConvention: DateConvention.Date,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
onGetErrorMessage: this.onEventEndDateValidation,
deferredValidationTime: 0,
key: 'eventEndDateId'
}),
PropertyPaneLabel('errorMessage', {
text: this.errorMessage,
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,157 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.Documentcard {
width: '100%';
height: '100%'
}
.previewEventIcon {
justify-content: 'center';
display: 'flex';
align-items: 'center';
font-size: 32px;
}
.locationIcon {
justify-content: 'center';
display: 'flex';
align-items: 'top';
font-size: 22px;
margin-top: 10px;
}
.location {
justify-content: 'center';
display: 'flex';
align-items: 'top';
}
.DocumentCardDetails{
background-color: #f2f2f2;
border-style: 'solid';
border-width: 1.3px;
border-color: #e6e6e6;
height: 40px;
justify-content: 'center';
display: 'flex';
align-items: 'top';
}
.eventStyle{
background-color: white;
border-radius: '0px';
opacity: 1;
color: black;
border-width: 1.4px;
border-style: 'solid';
display: 'block'
}
.DocumentCardTitle {
font-weight: 'bold';
height: '100%';
}
.DocumentCardTitleTime{
justify-content: 'center';
display: 'flex';
align-items: 'top';
margin-bottom: 6px;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-semibold;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
}
.calendar {
.container {
max-width: 100%;
min-height: 400px;
height: 600px;
margin: 0px auto;
}
.plainCard {
width: 300;
height: 286;
}
.eventTitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
border-style: solid
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,373 @@
import * as React from 'react';
import styles from './Calendar.module.scss';
import { ICalendarProps } from './ICalendarProps';
import { ICalendarState } from './ICalendarState';
import { escape } from '@microsoft/sp-lodash-subset';
import BigCalendar from 'react-big-calendar';
import * as moment from 'moment';
import * as strings from 'CalendarWebPartStrings';
import 'react-big-calendar/lib/css/react-big-calendar.css';
require('./calendar.css');
import {
IPersonaSharedProps,
Persona,
PersonaSize,
PersonaPresence,
HoverCard, IHoverCard, IPlainCardProps, HoverCardType, DefaultButton,
DocumentCard,
DocumentCardActivity,
DocumentCardDetails,
DocumentCardPreview,
DocumentCardTitle,
IDocumentCardPreviewProps,
IDocumentCardPreviewImage,
DocumentCardType,
Label,
ImageFit,
IDocumentCardLogoProps,
DocumentCardLogo,
DocumentCardImage,
Icon,
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
} from 'office-ui-fabric-react';
import { EnvironmentType } from '@microsoft/sp-core-library';
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from '@microsoft/sp-core-library';
import spservices from '../../../services/spservices';
import { stringIsNullOrEmpty } from '@pnp/common';
import { Event } from '../../../controls/Event/event';
import { IPanelModelEnum } from '../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../services/IEventData';
import { IUserPermissions } from './../../../services/IUserPermissions';
const localizer = BigCalendar.momentLocalizer(moment);
/**
* @export
* @class Calendar
* @extends {React.Component<ICalendarProps, ICalendarState>}
*/
export default class Calendar extends React.Component<ICalendarProps, ICalendarState> {
private spService: spservices = null;
private userListPermissions: IUserPermissions = undefined;
public constructor(props) {
super(props);
this.state = {
showDialog: false,
eventData: [],
selectedEvent: undefined,
isloading: true,
hasError: false,
errorMessage: '',
};
this.onDismissPanel = this.onDismissPanel.bind(this);
this.onSelectEvent = this.onSelectEvent.bind(this);
this.onSelectSlot = this.onSelectSlot.bind(this);
this.spService = new spservices(this.props.context);
moment.locale(this.props.context.pageContext.cultureInfo.currentUICultureName);
}
private onDocumentCardClick(ev: React.SyntheticEvent<HTMLElement, Event>) {
ev.preventDefault();
ev.stopPropagation();
}
/**
* @private
* @param {*} event
* @memberof Calendar
*/
private onSelectEvent(event: any) {
this.setState({ showDialog: true, selectedEvent: event, panelMode: IPanelModelEnum.edit });
}
/**
*
* @private
* @param {boolean} refresh
* @memberof Calendar
*/
private async onDismissPanel(refresh: boolean) {
this.setState({ showDialog: false });
if (refresh === true) {
this.setState({ isloading: true });
await this.loadEvents();
this.setState({ isloading: false });
}
}
/**
* @private
* @memberof Calendar
*/
private async loadEvents() {
try {
// Teste Properties
if (!this.props.list || !this.props.siteUrl || !this.props.eventStartDate.value || !this.props.eventEndDate.value) return;
this.userListPermissions = await this.spService.getUserPermissions(this.props.siteUrl, this.props.list);
const eventsData: IEventData[] = await this.spService.getEvents(escape(this.props.siteUrl), escape(this.props.list), this.props.eventStartDate.value, this.props.eventEndDate.value);
this.setState({ eventData: eventsData, hasError: false, errorMessage: "" });
} catch (error) {
this.setState({ hasError: true, errorMessage: error.message, isloading: false });
}
}
/**
* @memberof Calendar
*/
public async componentDidMount() {
this.setState({ isloading: true });
await this.loadEvents();
this.setState({ isloading: false });
}
/**
*
*
* @param {*} error
* @param {*} errorInfo
* @memberof Calendar
*/
public componentDidCatch(error: any, errorInfo: any) {
this.setState({ hasError: true, errorMessage: errorInfo.componentStack });
}
/**
*
*
* @param {ICalendarProps} prevProps
* @param {ICalendarState} prevState
* @memberof Calendar
*/
public async componentDidUpdate(prevProps: ICalendarProps, prevState: ICalendarState) {
if (!this.props.list || !this.props.siteUrl || !this.props.eventStartDate.value || !this.props.eventEndDate.value) return;
// Get Properties change
if (prevProps.list !== this.props.list || this.props.eventStartDate.value !== prevProps.eventStartDate.value || this.props.eventEndDate.value !== prevProps.eventEndDate.value) {
this.setState({ isloading: true });
await this.loadEvents();
this.setState({ isloading: false });
}
}
/**
* @private
* @param {*} { event }
* @returns
* @memberof Calendar
*/
private renderEvent({ event }) {
const previewEventIcon: IDocumentCardPreviewProps = {
previewImages: [
{
// previewImageSrc: event.ownerPhoto,
previewIconProps: { iconName: 'Calendar', styles: { root: { color: event.color } }, className: styles.previewEventIcon },
height: 43,
}
]
};
const EventInfo: IPersonaSharedProps = {
imageInitials: event.ownerInitial,
imageUrl: event.ownerPhoto,
text: event.title
};
/**
* @returns {JSX.Element}
*/
const onRenderPlainCard = (): JSX.Element => {
return (
<div className={styles.plainCard}>
<DocumentCard className={styles.Documentcard} >
<div>
<DocumentCardPreview {...previewEventIcon} />
</div>
<DocumentCardDetails>
<div className={styles.DocumentCardDetails}>
<DocumentCardTitle title={event.title} shouldTruncate={true} className={styles.DocumentCardTitle} styles={{ root: { color: event.color } }} />
</div>
{
moment(event.start).format('YYYY/MM/DD') !== moment(event.end).format('YYYY/MM/DD') ?
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('dddd')} - {moment(event.end).format('dddd')} </span>
:
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('dddd')} </span>
}
<span className={styles.DocumentCardTitleTime}>{moment(event.start).format('HH:mm')}H - {moment(event.end).format('HH:mm')}H</span>
<Icon iconName='MapPin' className={styles.locationIcon} style={{ color: event.color }} />
<DocumentCardTitle
title={`${event.location}`}
shouldTruncate={true}
showAsSecondaryTitle={true}
className={styles.location}
/>
<div style={{ marginTop: 20 }}>
<DocumentCardActivity
activity={strings.EventOwnerLabel}
people={[{ name: event.ownerName, profileImageSrc: event.ownerPhoto, initialsColor: event.color }]}
/>
</div>
</DocumentCardDetails>
</DocumentCard>
</div>
);
};
return (
<div style={{ height: 22 }}>
<HoverCard
cardDismissDelay={1000}
type={HoverCardType.plain}
plainCardProps={{ onRenderPlainCard: onRenderPlainCard }}
onCardHide={(): void => {
}}
>
<Persona
{...EventInfo}
size={PersonaSize.size24}
presence={PersonaPresence.none}
coinSize={22}
initialsColor={event.color}
/>
</HoverCard>
</div>
);
}
/**
*
*
* @private
* @memberof Calendar
*/
private onConfigure() {
// Context of the web part
this.props.context.propertyPane.open();
}
/**
* @param {*} { start, end }
* @memberof Calendar
*/
public async onSelectSlot({ start, end }) {
if (!this.userListPermissions.hasPermissionAdd) return;
this.setState({ showDialog: true, startDateSlot: start, endDateSlot: end, selectedEvent: undefined, panelMode: IPanelModelEnum.add });
}
/**
*
* @param {*} event
* @param {*} start
* @param {*} end
* @param {*} isSelected
* @returns {*}
* @memberof Calendar
*/
public eventStyleGetter(event, start, end, isSelected): any {
let style: any = {
backgroundColor: 'white',
borderRadius: '0px',
opacity: 1,
color: 'black',
borderWidth: '1.1px',
borderStyle: 'solid',
borderColor: event.color,
borderLeftWidth: '5px',
display: 'block'
};
return {
style: style
};
}
/**
*
* @returns {React.ReactElement<ICalendarProps>}
* @memberof Calendar
*/
public render(): React.ReactElement<ICalendarProps> {
return (
<div className={styles.calendar}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty} />
{
(!this.props.list || !this.props.eventStartDate.value || !this.props.eventEndDate.value) ?
<Placeholder iconName='Edit'
iconText={strings.WebpartConfigIconText}
description={strings.WebpartConfigDescription}
buttonLabel={strings.WebPartConfigButtonLabel}
hideButton={this.props.displayMode === DisplayMode.Read}
onConfigure={this.onConfigure.bind(this)} />
:
// test if has errors
this.state.hasError ?
<MessageBar messageBarType={MessageBarType.error}>
{this.state.errorMessage}
</MessageBar>
:
// show Calendar
// Test if is loading Events
<div>
{this.state.isloading ? <Spinner size={SpinnerSize.large} label={strings.LoadingEventsLabel} /> :
<div className={styles.container}>
<BigCalendar
localizer={localizer}
selectable
events={this.state.eventData}
startAccessor="start"
endAccessor="end"
eventPropGetter={this.eventStyleGetter}
onSelectSlot={this.onSelectSlot}
components={{
event: this.renderEvent
}}
onSelectEvent={this.onSelectEvent}
defaultDate={moment().startOf('day').toDate()}
messages={
{
'today': strings.todayLabel,
'previous': strings.previousLabel,
'next': strings.nextLabel,
'month': strings.monthLabel,
'week': strings.weekLabel,
'day': strings.dayLable,
'showMore': total => `+${total} ${strings.showMore}`
}
}
/>
</div>
}
</div>
}
{
this.state.showDialog &&
<Event
event={this.state.selectedEvent}
panelMode={this.state.panelMode}
onDissmissPanel={this.onDismissPanel}
showPanel={this.state.showDialog}
startDate={this.state.startDateSlot}
endDate={this.state.endDateSlot}
context={this.props.context}
siteUrl={this.props.siteUrl}
listId={this.props.list}
/>
}
</div>
);
}
}

View File

@ -0,0 +1,13 @@
import { DisplayMode } from '@microsoft/sp-core-library';
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { IDateTimeFieldValue } from '@pnp/spfx-property-controls/lib/PropertyFieldDateTimePicker';
export interface ICalendarProps {
title: string;
siteUrl: string;
list: string;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
context: WebPartContext;
eventStartDate: IDateTimeFieldValue;
eventEndDate: IDateTimeFieldValue;
}

View File

@ -0,0 +1,13 @@
import { IPanelModelEnum} from '../../../controls/Event/IPanelModeEnum';
import { IEventData } from './../../../services/IEventData';
export interface ICalendarState {
showDialog: boolean;
eventData: IEventData[];
selectedEvent: IEventData;
panelMode?: IPanelModelEnum;
startDateSlot?: Date;
endDateSlot?:Date;
isloading: boolean;
hasError: boolean;
errorMessage: string;
}

View File

@ -0,0 +1,30 @@
import React from 'react';
import Toolbar from 'react-big-calendar/lib/Toolbar';
export default class CalendarToolbar extends Toolbar {
componentDidMount() {
const view = this.props.view;
console.log(view)
}
render() {
return (
<div>
<div className="rbc-btn-group">
<button type="button" onClick={() => this.navigate('TODAY')}>today</button>
<button type="button" onClick={() => this.navigate('PREV')}>back</button>
<button type="button" onClick={() => this.navigate('NEXT')}>next</button>
</div>
<div className="rbc-toolbar-label">{this.props.label}</div>
<div className="rbc-btn-group">
<button type="button" onClick={this.view.bind(null, 'month')}>Month</button>
<button type="button" onClick={this.view.bind(null, 'week')}>Week</button>
<button type="button" onClick={this.view.bind(null, 'day')}>Day</button>
<button type="button" onClick={this.view.bind(null, 'agenda')}>Agenda</button>
</div>
</div>
);
}
}
module.export = CalendarToolbar;

View File

@ -0,0 +1,92 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.rbc-selected-cell {
background-color: $ms-color-themeDark;
}
.rbc-event:hover {
border-width: 2px;
}
.rbc-today {
background-color: $ms-color-themeDark;
opacity: 0.7;
}
.rbc-off-range-bg {
background: #f4f4f4;
}
.rbc-toolbar button {
color: #373a3c;
display: inline-block;
margin: 0;
text-align: center;
vertical-align: middle;
background: none;
background-image: none;
border: 1px solid #ccc;
padding: .375rem 1rem;
/* border-radius: 4px; */
line-height: normal;
white-space: nowrap;
}
.rbc-toolbar button:active {
background-image: none;
/* box-shadow: inset 0 3px 5px rgba(0,0,0,.125); */
background-color: #f8f8f8;
border-color: #adadad;
}
.rbc-toolbar button {
color: #373a3c;
display: inline-block;
margin: 0;
text-align: center;
vertical-align: middle;
background: none;
background-image: none;
border: 1px solid #ccc;
padding: .375rem 1rem;
border-radius: 0px;
line-height: normal;
white-space: nowrap;
}
.rbc-toolbar button.rbc-active {
background-image: none;
background-color: $ms-color-themeDark;
/* background-color: #0075c7;*/
border-color: #f4f4f4;
color: white;
}
.rbc-toolbar button.rbc-active:focus, .rbc-toolbar button.rbc-active:hover, .rbc-toolbar button:active:focus, .rbc-toolbar button:active:hover {
color: #fff;
background-color: $ms-color-themeDark;
border-color: #f4f4f4;
}
.rbc-toolbar button:focus, .rbc-toolbar button:hover {
color: #f8f8f8;
background-color: $ms-color-themeDark;
border-color: #adadad;
}
.rbc-show-more {
background-color: hsla(0,0%,100%,.3);
z-index: 4;
font-weight: 700;
font-size: 85%;
height: auto;
line-height: normal;
white-space: nowrap;
}
.rbc-ellipsis, .rbc-event-label, .rbc-row-segment .rbc-event-content, .rbc-show-more {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -0,0 +1,96 @@
define([], function () {
return {
PropPanelSiteUrlErrorMessage:'Please verify if site url is valid',
HttpErrorMessage: "Error reading calendar events:",
CategoryPlaceHolder: "Please select category",
CategoryLabel: "Category",
EnDateValidationMessage: "start date is greater than end date",
SartDateValidationMessage: "start date is greater than end date",
eventSelectDatesLabel: "Show only the events within the following dates",
ConfirmeDeleteMessage: "Confirm delete event ?",
DialogConfirmDeleteTitle: " 'Delete Event'",
SpinnerDeletingLabel: "Deleting...",
DialogCloseButtonLabel: "Cancel",
DialogConfirmDeleteLabel: "Delete",
SaveButtonLabel: " Save",
DeleteButtonLabel: "Delete",
CancelButtonLabel: "Cancel",
LoadingEventsLabel: "Loading events...",
WebPartConfigButtonLabel: "Configure",
WebpartConfigDescription: "Please configure list calendar ",
WebpartConfigIconText: "Configure your Calendar Web Part",
EventOwnerLabel: "event owner",
InvalidDateFormat: "Invalid date format.",
IsRequired: "Field is required.",
CloseDate: "Close date picker",
NextYear: "Go to next year",
PrevYear: "Go to previous year",
NextMonth: "Go to next month",
PrevMonth: "Go to previous month",
GoToDay: "Go to today",
ShortDay_Saunday: "S",
ShortDay_Friday: "F",
ShortDay_Tursday: "T",
ShortDay_W: "W",
ShortDay_T: "T",
ShortDay_M: "M",
ShortDay_S: "S",
Saturday: "Saturday",
Friday: "Friday",
Thursday: "Thursday",
Wednesday: "Wednesday",
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',
Dezember: "December",
November: " 'November'",
October: "October",
September: "September",
August: " 'August'",
July: "July",
June: "June",
May: "May",
April: "April",
March: "March",
February: "February",
January: "January",
LocationLabel: "Location search and Map",
LocationTextLabel: "Location",
AttendeesLabel: "Attendees",
EndMinLabel: "Min",
EndHourLabel: "Hour",
EndDateLabel: "End Date",
EndDatePlaceHolder: "Select a date...",
StartMinLabel: "Min",
StartHourLabel: "Hour",
StartDateLabel: "Start Date",
StartDatePlaceHolder: "Select a date...",
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'
}
});

View File

@ -0,0 +1,100 @@
declare interface ICalendarWebPartStrings {
HttpErrorMessage: string;
CategoryPlaceHolder: string;
CategoryLabel: string;
EnDateValidationMessage: string;
SartDateValidationMessage: string;
eventSelectDatesLabel: string;
ConfirmeDeleteMessage: string;
DialogConfirmDeleteTitle: string;
SpinnerDeletingLabel: string;
DialogCloseButtonLabel: string;
DialogConfirmDeleteLabel: string;
SaveButtonLabel: string;
DeleteButtonLabel: string;
CancelButtonLabel: string;
LoadingEventsLabel: string;
WebPartConfigButtonLabel: string;
WebpartConfigDescription: string;
WebpartConfigIconText: string;
EventOwnerLabel: string;
InvalidDateFormat: string;
IsRequired: string;
CloseDate: string;
NextYear: string;
PrevYear: string;
NextMonth: string;
PrevMonth: string;
GoToDay: string;
ShortDay_Saunday: string;
ShortDay_Friday: string;
ShortDay_Tursday: string;
ShortDay_W: string;
ShortDay_T: string;
ShortDay_M: string;
ShortDay_S: string;
Saturday: string;
Friday: string;
Thursday: string;
Wednesday: string;
Tuesday: string;
Monday: string;
Sunday: string;
Jan:string;
Feb:string;
Mar:string;
Apr:string;
May:string,
Jun:string;
Jul:string;
Aug:string;
Sep:string;
Oct:string;
Nov:string;
Dez:string;
Dezember: string;
November: string;
October: string;
September: string;
August: string;
July: string;
June: string;
May: string;
April: string;
March: string;
February: string;
January: string;
LocationLabel: string;
LocationTextLabel: string;
AttendeesLabel: string;
EndMinLabel: string;
EndHourLabel: string;
EndDateLabel: string;
EndDatePlaceHolder: string;
StartMinLabel: string;
StartHourLabel: string;
StartDateLabel: string;
StartDatePlaceHolder: string;
EventTitleErrorMessage: string;
EventTitleLabel: string;
EventPanelTitle: string;
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
SiteUrlFieldLabel: string;
ListFieldLabel: string;
monthLabel: string;
weekLabel: string;
dayLable: string;
agenda: string;
todayLabel: string;
previousLabel: string;
nextLabel: string;
showMore: string;
PropPanelSiteUrlErrorMessage: string;
}
declare module 'CalendarWebPartStrings' {
const strings: ICalendarWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,96 @@
define([], function() {
return {
PropPanelSiteUrlErrorMessage:'Por favor verifique se site url é valido.',
HttpErrorMessage: "Error reading calendar events:",
CategoryPlaceHolder: "Please select category",
CategoryLabel: "Category",
EnDateValidationMessage: "start date is greater than end date",
SartDateValidationMessage: "start date is greater than end date",
eventSelectDatesLabel: "Show only the events within the following dates",
ConfirmeDeleteMessage: "Confirm delete event ?",
DialogConfirmDeleteTitle: " 'Delete Event'",
SpinnerDeletingLabel: "Deleting...",
DialogCloseButtonLabel: "Cancel",
DialogConfirmDeleteLabel: "Delete",
SaveButtonLabel: " Save",
DeleteButtonLabel: "Delete",
CancelButtonLabel: "Cancel",
LoadingEventsLabel: "A carregar eventos...",
WebPartConfigButtonLabel: "Configurar",
WebpartConfigDescription: "Por favor configure configurar calendário ",
WebpartConfigIconText: "Configurar Web Part Calendarário",
EventOwnerLabel: "Organizador",
InvalidDateFormat: "Formato data inválido",
IsRequired: "Campo é obrigatório",
CloseDate: "Fechar date picker",
NextYear: "Ir para o próximo ano",
PrevYear: "Ir para o ano anterior",
NextMonth: "Ir para o próximo mês",
PrevMonth: "Ir para o mês anterior",
GoToDay: "Ir para dia de hoje",
ShortDay_Saunday: "S",
ShortDay_Friday: "S",
ShortDay_Tursday: "Q",
ShortDay_W: "Q",
ShortDay_T: "T",
ShortDay_M: "S",
ShortDay_S: "D",
Saturday: "Sábado",
Friday: "Sexta-feira",
Thursday: "Quinta-feira",
Wednesday: "Quarta",
Tuesday: "Terça",
Monday: "Segunda",
Sunday: "Domingo",
Jan:'Jan',
Feb:'Fev',
Mar:'Mar',
Apr:'Abr',
May:'Mai',
Jun:'Jun',
Jul:'Jul',
Aug:'Aug',
Sep:'Sep',
Oct:'Oct',
Nov:'Nov',
Dez:'Dez',
Dezember: "Dezembro",
November: " 'Novembro'",
October: "Outubro",
September: "Setembro",
August: 'Agosto',
July: "Julho",
June: "Junho",
May: "Maio",
April: "Abril",
March: "Março",
February: "Fevereiro",
January: "Janeiro",
LocationLabel: "Mapa",
LocationTextLabel: "Local",
AttendeesLabel: "Participantes",
EndMinLabel: "Min",
EndHourLabel: "Hora",
EndDateLabel: "Data fim",
EndDatePlaceHolder: "Seleccionar data...",
StartMinLabel: "Min",
StartHourLabel: "Hora",
StartDateLabel: "Data Início",
StartDatePlaceHolder: "Seleccionar data...",
EventTitleErrorMessage: "Título do evento é obrigatório.",
EventTitleLabel: "Título do evento",
EventPanelTitle: "Editar/Addcionar Evento",
"PropertyPaneDescription": "Calendário com marcação de eventos, mostra os eventos criados na lista calendário definida no site seleccionado",
"BasicGroupName": "Indique o Url do site e calendário",
"SiteUrlFieldLabel": 'Url do Site',
"ListFieldLabel": 'Nome da Lista Calendario',
"weekLabel": 'Semana',
"dayLable": 'Dia',
"agenda": 'Agenda',
"monthLabel": 'Mês',
"todayLabel": 'Hoje',
"previousLabel": 'Anterior',
"nextLabel": "Seguinte",
"showMore": 'mais'
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,64 @@
/**
* This script updates the package-solution version analogue to the
* the package.json file.
*/
if (process.env.npm_package_version === undefined) {
throw 'Package version cannot be evaluated';
}
// define path to package-solution file
const solution = './config/package-solution.json',
teams = './teams/manifest.json';
// require filesystem instanc
const fs = require('fs');
// get next automated package version from process variable
const nextPkgVersion = process.env.npm_package_version;
// make sure next build version match
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
nextPkgVersion : nextPkgVersion.split('-')[0];
// Update version in SPFx package-solution if exists
if (fs.existsSync(solution)) {
// read package-solution file
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
// parse file as json
const solutionContents = JSON.parse(solutionFileContent);
// set property of version to next version
solutionContents.solution.version = nextVersion + '.0';
// save file
fs.writeFileSync(
solution,
// convert file back to proper json
JSON.stringify(solutionContents, null, 2),
'UTF-8');
}
// Update version in teams manifest if exists
if (fs.existsSync(teams)) {
// read package-solution file
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
// parse file as json
const teamsContent = JSON.parse(teamsManifestContent);
// set property of version to next version
teamsContent.version = nextVersion;
// save file
fs.writeFileSync(
teams,
// convert file back to proper json
JSON.stringify(teamsContent, null, 2),
'UTF-8');
}

View File

@ -1,4 +1,5 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.2/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
@ -10,6 +11,9 @@
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"

Some files were not shown because too many files have changed in this diff Show More