This commit is contained in:
VesaJuvonen 2020-02-10 11:38:35 +02:00
commit 0578d52dc0
131 changed files with 85328 additions and 9721 deletions

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.9.1",
"libraryName": "modern-page-comments",
"libraryId": "cfa8dbcf-b32d-4de0-a6ba-af486007c2f2",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,88 @@
# Advanced Comments Box
## Summary
> This component is developed for the advanced usage of commenting the page or article etc. **_Page Comments_** lists will be created to store the comments. Following are some of the features of this component.
* Can be used in the modern page with the existing comments disabled.
* Classification of comments by **_Popular_**, **_Newest_**, **_Oldest_** and **_Attachments_**
* Ability to refer files as a comment.
* **_Edit_**, **_Reply_** (nested comments), **_Like_** & **_Delete_** options are available based on the configuration.
* **_Hashtag_** & **_Ping Users_** are also available.
* **_Document Preview_** is also available for all office documents and videos based on the configuration.
* Display of **_New_** icon for the current day comments.
## Properties
1. **_DateTime_** format on when the comments were added or modified
2. **_Profile Picture_** style, whether it has to be rounded or square
3. Enable or Disable **_Navigation_** whether to display the comments classification
4. Enable or disable **_Attachments_**. Following properties are required when attachments are enabled.
* **_Library_** to store the files uploaded.
* Allowed **_File Formats_** in the comments box.
* Maximum **_File Size_** allowed.
5. **_Ping Users_** will allow to mention the users. The users are pulled from the **Site Users**.
6. **_Edit_** comments can be enabled or disabled to allow the users to edit the comments. Files added can be deleted not edited.
* **_Delete_** option can be enabled or disabled to allow the users to delete the comments. Comments with no-replies are allowed to delete. Delete is allowed only if Edit is allowed.
7. **_Upvoting_** of comments to like or dislike the comments.
8. **_Hashtags_**
9. **_Document Preview_** can be enabled or disabled for the office files and videos.
## Preview
![Advanced-Comments-Box](./assets/Advanced-Comments-Box.gif)
## 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)
## Prerequisites
> **@microsoft/generator-sharepoint - 1.9.1**
## Solution
Solution|Author(s)
--------|---------
SPFxPageComments | Sudharsan K.([@sudharsank](https://twitter.com/sudharsank), [Know More](http://windowssharepointserver.blogspot.com/))
## Version history
Version|Date|Comments
-------|----|--------
1.0.0.0|Feb 05 2020|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 bundle --ship && gulp package-solution --ship`
- Add the .sppkg file to the app catalog and add the **Page Comments** web part to the page.
## Features
- Used [SharePoint Framework Property Controls](https://sharepoint.github.io/sp-dev-fx-property-controls/) to create the property pane controls(Text, ListPicker, Toggle) with callout.
- Used [PnP](https://pnp.github.io/pnpjs/) for communication with SharePoint.
- Used [jquery-comments](https://viima.github.io/jquery-comments/) for comments control with some customization.
- Used [Moment.js](https://momentjs.com/) for datetime formatting.
#### Local Mode
This solution doesn't work on local mode.
#### SharePoint Mode
If you want to try on a real environment, open:
[O365 Workbench](https://your-domain.sharepoint.com/_layouts/15/workbench.aspx)

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

View File

@ -0,0 +1,31 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"page-comments-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/pageComments/PageCommentsWebPart.js",
"manifest": "./src/webparts/pageComments/PageCommentsWebPart.manifest.json"
}
]
}
},
"externals": {
"jquery": {
"path": "node_modules/jquery/dist/jquery.min.js",
"globalName": "jQuery"
},
"textcomplete": {
"path": "https://cdnjs.cloudflare.com/ajax/libs/jquery.textcomplete/1.8.0/jquery.textcomplete.js",
"globalName": "jQuery",
"globalDependencies": [
"jquery"
]
}
},
"localizedResources": {
"PageCommentsWebPartStrings": "lib/webparts/pageComments/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "Advanced Comments Box",
"id": "cfa8dbcf-b32d-4de0-a6ba-af486007c2f2",
"version": "1.1.0.2",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/advanced-comments-box.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
{
"name": "modern-page-comments",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.9.1",
"@microsoft/sp-lodash-subset": "1.9.1",
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
"@microsoft/sp-webpart-base": "1.9.1",
"@pnp/sp": "^2.0.0",
"@pnp/spfx-property-controls": "1.16.0",
"@types/es6-promise": "0.0.33",
"@types/jquery": "^2.0.54",
"@types/webpack-env": "1.13.1",
"jquery": "^2.2.4",
"moment": "^2.24.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.9.1",
"@microsoft/sp-tslint-rules": "1.9.1",
"@microsoft/sp-module-interfaces": "1.9.1",
"@microsoft/sp-webpart-workbench": "1.9.1",
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

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

View File

@ -0,0 +1,33 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "7520fa3d-1f05-4d78-bf7f-91627d9b64d3",
"alias": "PageCommentsWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "Advanced Comments" },
"description": { "default": "Comments with advanced features for modern pages" },
"officeFabricIconFontName": "FileComment",
"properties": {
"datetimeFormat":"DD/MM/YYYY",
"enableNavigation": true,
"enableReplying": true,
"enableEditing": false,
"enableUpvoting": true,
"enableDeleting": false,
"enableAttachments": false,
"enableHashtags": false,
"enablePinging": false,
"enableDocumentPreview": false,
"roundProfilePictures": true,
"attachmentFileFormats": "audio/*,image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx",
"attachmentFileSize": 2
}
}]
}

View File

@ -0,0 +1,44 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.pageComments {
.container {
// max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
}
.errorMessage {
margin: 10px;
border: 1px solid red;
padding: 10px;
background-color: #ead9d9;
font-weight: 600;
}

View File

@ -0,0 +1,374 @@
import * as React from 'react';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import { SPComponentLoader } from '@microsoft/sp-loader';
import { CalloutTriggers } from '@pnp/spfx-property-controls/lib/PropertyFieldHeader';
import { PropertyFieldSliderWithCallout } from '@pnp/spfx-property-controls/lib/PropertyFieldSliderWithCallout';
import { PropertyFieldToggleWithCallout } from '@pnp/spfx-property-controls/lib/PropertyFieldToggleWithCallout';
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
import * as _ from "lodash";
import * as moment from 'moment';
import styles from './PageCommentsWebPart.module.scss';
import * as strings from 'PageCommentsWebPartStrings';
import * as $ from 'jquery';
require('textcomplete');
import { sp } from '@pnp/sp';
import SPHelper from './SPHelper';
require('./css/jquery-comments.css');
export interface IPageCommentsWebPartProps {
enableNavigation: boolean;
enableReplying: boolean;
enableAttachments: boolean;
enableEditing: boolean;
enableUpvoting: boolean;
enableDeleting: boolean;
enableDeletingCommentWithReplies: boolean;
enableHashtags: boolean;
enablePinging: boolean;
enableDocumentPreview: boolean;
roundProfilePictures: boolean;
datetimeFormat: string;
attachmentFileFormats: string;
attachmentFileSize: number;
docLib: string;
}
export default class PageCommentsWebPart extends BaseClientSideWebPart<IPageCommentsWebPartProps> {
private helper: SPHelper = null;
private currentUserInfo: any = null;
private siteUsers: any[] = [];
private pageurl: string = '';
private postAttachmentPath: string = '';
private pageFolderExists: boolean = false;
protected async onInit(): Promise<void> {
await super.onInit();
sp.setup(this.context);
}
public constructor() {
super();
SPComponentLoader.loadCss("https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css");
}
public render(): void {
if (this.properties.enableAttachments && (this.properties.docLib === null || undefined === this.properties.docLib ||
this.properties.docLib.toLocaleUpperCase() === "NO_LIST_SELECTED")) {
this.domElement.innerHTML = `
<div class="${styles.errorMessage}"><i class="fa fa-times-circle" aria-hidden="true"></i>&nbsp;${strings.NoAttachmentRepoMsg}</div>
`;
} else {
this.context.statusRenderer.displayLoadingIndicator(this.domElement, strings.LoadingMsg, 0);
this.checkAndCreateList();
}
}
private async checkAndCreateList() {
if (this.properties.enableAttachments) {
this.helper = new SPHelper(this.properties.docLib);
} else {
this.helper = new SPHelper();
}
await this.helper.checkListExists();
this.initializeComments();
}
private initializeComments = async () => {
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
this.domElement.innerHTML = `
<div class="${ styles.pageComments}">
<div class="${ styles.container}">
<div class="${ styles.row}">
<div id="page-comments"></div>
</div>
</div>
</div>`;
var self = this;
if (this.properties.enableAttachments) {
await this.helper.getDocLibInfo();
this.postAttachmentPath = await this.helper.getPostAttachmentFilePath(this.pageurl);
this.pageFolderExists = await this.helper.checkForPageFolder(this.postAttachmentPath);
}
this.pageurl = this.context.pageContext.legacyPageContext.serverRequestPath;
this.currentUserInfo = await this.helper.getCurrentUserInfo();
this.siteUsers = await this.helper.getSiteUsers(self.currentUserInfo.ID);
require(['jquery', './js/jquery-comments.min'], (jQuery, comments) => {
jQuery('#page-comments').comments({
profilePictureURL: self.currentUserInfo.Picture,
currentUserId: self.currentUserInfo.ID,
maxRepliesVisible: 3,
textareaRows: 1,
textareaRowsOnFocus: 2,
textareaMaxRows: 5,
highlightColor: '#b5121b',
attachmentFileFormats: self.properties.attachmentFileFormats !== undefined ? self.properties.attachmentFileFormats : 'audio/*,image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx',
attachmentFileSize: self.properties.attachmentFileSize !== undefined ? self.properties.attachmentFileSize : 5,
siteURL: self.context.pageContext.legacyPageContext.webServerRelativeUrl,
enableNavigation: self.properties.enableNavigation !== undefined ? self.properties.enableNavigation : true,
enableReplying: self.properties.enableReplying !== undefined ? self.properties.enableReplying : true,
enableEditing: self.properties.enableEditing !== undefined ? self.properties.enableEditing : false,
enableUpvoting: self.properties.enableUpvoting !== undefined ? self.properties.enableUpvoting : true,
enableDeleting: self.properties.enableDeleting !== undefined ? self.properties.enableDeleting : false,
enableAttachments: self.properties.enableAttachments !== undefined ? self.properties.enableAttachments : false,
enableHashtags: self.properties.enableHashtags !== undefined ? self.properties.enableHashtags : false,
enablePinging: self.properties.enablePinging !== undefined ? self.properties.enablePinging : false,
enableDocumentPreview: self.properties.enableDocumentPreview !== undefined ? self.properties.enableDocumentPreview : false,
roundProfilePictures: self.properties.roundProfilePictures !== undefined ? self.properties.roundProfilePictures : true,
timeFormatter: (time) => {
try {
if (self.properties.datetimeFormat) {
return moment(time).format(self.properties.datetimeFormat);
} else return moment(time).format(self.properties.datetimeFormat);
} catch (err) {
return moment(time).format("DD/MM/YYYY hh:mm:ss A");
}
},
getComments: async (success, error) => {
let commentsArray = await self.helper.getPostComments(self.pageurl, self.currentUserInfo);
if (commentsArray.length > 0) {
var fil = _.filter(commentsArray, (o) => { return moment(o.created).format("DD/MM/YYYY") === moment().format("DD/MM/YYYY"); });
fil.map((comment) => {
_.set(comment, 'is_new', true);
});
fil = _.filter(commentsArray, (o) => { return o.userid == self.currentUserInfo.ID; });
fil.map((comment) => {
_.set(comment, 'created_by_current_user', true);
});
}
success(commentsArray);
},
postComment: async (commentJson, success, error) => {
commentJson.fullname = self.currentUserInfo.DisplayName;
commentJson.userid = self.currentUserInfo.ID;
commentJson = self.saveComment(commentJson);
await self.helper.postComment(self.pageurl, commentJson, self.currentUserInfo);
if (moment(commentJson.created).format("DD/MM/YYYY") === moment().format("DD/MM/YYYY")) _.set(commentJson, 'is_new', true);
_.set(commentJson, 'created_by_current_user', true);
success(commentJson);
},
searchUsers: async (term, success, error) => {
let res = [];
if (self.siteUsers.length <= 0) self.siteUsers = await self.helper.getSiteUsers(self.currentUserInfo.ID);
res = _.chain(self.siteUsers).filter((o) => { return o.fullname.toLowerCase().indexOf(term) >= 0 || o.email.toLowerCase().indexOf(term) >= 0; }).take(10).value();
success(res);
},
upvoteComment: async (commentJSON, success, error) => {
await self.helper.voteComment(self.pageurl, commentJSON, self.currentUserInfo);
success(commentJSON);
},
deleteComment: async (commentJSON, success, error) => {
await self.helper.deleteComment(self.pageurl, commentJSON);
success();
},
putComment: async (commentJSON, success, error) => {
commentJSON = self.saveComment(commentJSON);
await self.helper.editComments(self.pageurl, commentJSON);
success(commentJSON);
},
uploadAttachments: async (commentArray, success, error) => {
let res = await self.helper.postAttachments(commentArray, self.pageFolderExists, self.postAttachmentPath);
_.merge(res[0], { userid: self.currentUserInfo.ID, fullname: self.currentUserInfo.DisplayName });
await self.helper.postComment(self.pageurl, res[0], self.currentUserInfo);
if (moment(res[0].created).format("DD/MM/YYYY") === moment().format("DD/MM/YYYY")) _.set(res[0], 'is_new', true);
_.set(res[0], 'created_by_current_user', true);
success(res);
}
});
});
}
private saveComment = (data) => {
// Convert pings to human readable format
$(Object.keys(data.pings)).each((index, userId) => {
var fullname = data.pings[`${userId}`];
var pingText = '@' + fullname;
data.content = data.content.replace(new RegExp('@' + userId, 'g'), pingText);
});
return data;
}
private checkForDocumentLibrary = (value: string): string => {
if (value === null || value.trim().length === 0 || value.toLocaleUpperCase() === "NO_LIST_SELECTED") {
return strings.AttachmentRepoPropValMsg;
}
return '';
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('datetimeFormat', {
label: strings.DateTimeFormatLabel,
description: strings.DateTimeFormatDescription,
multiline: false,
resizable: false,
value: this.properties.datetimeFormat
}),
PropertyFieldToggleWithCallout('roundProfilePictures', {
calloutTrigger: CalloutTriggers.Hover,
key: 'roundProfilePicturesFieldId',
label: strings.RoundProfilePicLabel,
calloutContent: React.createElement('p', {}, strings.RoundProfilePicDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.roundProfilePictures !== undefined ? this.properties.roundProfilePictures : true
}),
PropertyFieldToggleWithCallout('enableNavigation', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableNavigationFieldId',
label: strings.NavigationLabel,
calloutContent: React.createElement('p', {}, strings.NavigationDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableNavigation !== undefined ? this.properties.enableNavigation : true
}),
PropertyFieldToggleWithCallout('enableAttachments', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableAttachmentsFieldId',
label: strings.AttachmentLabel,
calloutContent: React.createElement('p', {}, strings.AttachmentDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableAttachments !== undefined ? this.properties.enableAttachments : false
}),
PropertyFieldListPicker('docLib', {
label: strings.AttachmentRepoLabel,
selectedList: this.properties.docLib,
includeHidden: false,
orderBy: PropertyFieldListPickerOrderBy.Title,
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
properties: this.properties,
context: this.context,
onGetErrorMessage: this.checkForDocumentLibrary.bind(this),
deferredValidationTime: 0,
key: 'docLibFieldId',
baseTemplate: 101,
disabled: !this.properties.enableAttachments
}),
PropertyPaneTextField('attachmentFileFormats', {
label: strings.AttachmentFileFormatLabel,
description: strings.AttachmentFileFormatDescription,
multiline: false,
resizable: false,
value: this.properties.attachmentFileFormats,
disabled: !this.properties.enableAttachments
}),
PropertyFieldSliderWithCallout('attachmentFileSize', {
calloutContent: React.createElement('div', {}, strings.AttachmentFileSizeDescription),
calloutTrigger: CalloutTriggers.Hover,
calloutWidth: 200,
key: 'attachmentFileSizeFieldId',
label: strings.AttachmentFileSizeLabel,
max: 10,
min: 1,
step: 1,
showValue: true,
value: this.properties.attachmentFileSize,
disabled: !this.properties.enableAttachments
}),
PropertyFieldToggleWithCallout('enablePinging', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enablePingingFieldId',
label: strings.PingLabel,
calloutContent: React.createElement('p', {}, strings.PingDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enablePinging !== undefined ? this.properties.enablePinging : false
}),
PropertyFieldToggleWithCallout('enableEditing', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableEditingFieldId',
label: strings.EditingLabel,
calloutContent: React.createElement('p', {}, strings.EditingDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableEditing !== undefined ? this.properties.enableEditing : false
}),
PropertyFieldToggleWithCallout('enableDeleting', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableDeletingFieldId',
label: strings.DeleteLabel,
calloutContent: React.createElement('p', {}, strings.DeleteDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableDeleting !== undefined ? this.properties.enableDeleting : false,
disabled: !this.properties.enableEditing
}),
// PropertyFieldToggleWithCallout('enableDeletingCommentWithReplies', {
// calloutTrigger: CalloutTriggers.Hover,
// key: 'enableDeletingCommentWithRepliesFieldId',
// label: strings.DeleteRepliesLabel,
// calloutContent: React.createElement('p', {}, strings.DeleteRepliesDescription),
// onText: 'Enable',
// offText: 'Disable',
// checked: this.properties.enableDeletingCommentWithReplies,
// disabled: !this.properties.enableEditing
// }),
PropertyFieldToggleWithCallout('enableUpvoting', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableUpvotingFieldId',
label: strings.UpVotingLabel,
calloutContent: React.createElement('p', {}, strings.UpVotingDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableUpvoting !== undefined ? this.properties.enableUpvoting : true
}),
PropertyFieldToggleWithCallout('enableReplying', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableReplyingFieldId',
label: strings.ReplyLabel,
calloutContent: React.createElement('p', {}, strings.ReplyDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableReplying !== undefined ? this.properties.enableReplying : true
}),
PropertyFieldToggleWithCallout('enableHashtags', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableHashtagsFieldId',
label: strings.HashtagsLabel,
calloutContent: React.createElement('p', {}, strings.HashtagsDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableHashtags !== undefined ? this.properties.enableHashtags : false
}),
PropertyFieldToggleWithCallout('enableDocumentPreview', {
calloutTrigger: CalloutTriggers.Hover,
key: 'enableDocumentPreviewFieldId',
label: strings.DocumentPreviewLabel,
calloutContent: React.createElement('p', {}, strings.DocumentPreviewDescription),
onText: 'Enable',
offText: 'Disable',
checked: this.properties.enableDocumentPreview !== undefined ? this.properties.enableDocumentPreview : false
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,281 @@
import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";
import "@pnp/sp/folders/web";
import "@pnp/sp/files/folder";
import "@pnp/sp/items/list";
import "@pnp/sp/fields/list";
import "@pnp/sp/views/list";
import "@pnp/sp/site-users/web";
import { IList } from "@pnp/sp/lists";
import * as _ from "lodash";
export default class SPHelper {
private lst_pageComments: string = '';
private lst_pageDocuments: string = '';
private lst_docListName: string = '';
private _list: IList = null;
private _doclist: IList = null;
private cqPostDocs: string = `<View>
<Query>
<Where>
<And>
<Eq>
<FieldRef Name='FSObjType' />
<Value Type='Text'>1</Value>
</Eq>
<Eq>
<FieldRef Name='FileRef' />
<Value Type='Text'>{{FilePath}}</Value>
</Eq>
</And>
</Where>
<ViewFields><FieldRef Name="ID" /></ViewFields>
</Query>
</View>`;
public constructor(lstDocLib?: string) {
this.lst_pageComments = "Page Comments";
this._list = sp.web.lists.getByTitle(this.lst_pageComments);
if (lstDocLib) {
this.lst_pageDocuments = lstDocLib;
}
}
public getDocLibInfo = async () => {
this._doclist = sp.web.lists.getById(this.lst_pageDocuments);
let listInfo: any = await this._doclist.select('Title').get();
this.lst_docListName = listInfo.Title;
}
public queryList = async (query, $list: IList) => {
return await $list.getItemsByCAMLQuery(query);
}
public getCurrentUserInfo = async () => {
let currentUserInfo = await sp.web.currentUser.get();
return ({
ID: currentUserInfo.Id,
Email: currentUserInfo.Email,
LoginName: currentUserInfo.LoginName,
DisplayName: currentUserInfo.Title,
Picture: '/_layouts/15/userphoto.aspx?size=S&username=' + currentUserInfo.UserPrincipalName,
});
}
public getSiteUsers = async (currentUserId: number) => {
let resusers = await sp.web.siteUsers.filter('IsHiddenInUI eq false and PrincipalType eq 1').get();
_.remove(resusers, (o) => { return o.Id == currentUserId || o.Email == ""; });
let userResults = [];
resusers.map((user) => {
userResults.push({
id: user.Id,
fullname: user.Title,
email: user.Email,
profile_picture_url: '/_layouts/15/userphoto.aspx?size=S&username=' + user.UserPrincipalName
});
});
return userResults;
}
public getPostAttachmentFilePath = async (pageUrl) => {
let pageName = pageUrl.split('/')[pageUrl.split('/').length - 1].split('.').slice(0, -1).join('.');
let res = await sp.web.select('ServerRelativeUrl').get();
let doclistName = (this.lst_docListName.toLowerCase() === 'documents') ? "Shared Documents" : this.lst_docListName;
return res.ServerRelativeUrl + "/" + doclistName + "/" + pageName;
}
public checkForPageFolder = async (postAttachmentPath) => {
let xml = this.cqPostDocs.replace('{{FilePath}}', postAttachmentPath);
let q = {
ViewXml: xml
};
let res = await this.queryList(q, this._doclist);
if (res.length > 0) return true; else return false;
}
public getPostComments = async (pageurl, currentUserInfo) => {
let pagecomments = await this._list.items.select('Comments', 'Likes', 'FieldValuesAsText/Comments', 'FieldValuesAsText/Likes')
.filter(`PageURL eq '${pageurl}'`).expand('FieldValuesAsText').get();
if (pagecomments.length > 0) {
var tempComments = pagecomments[0].FieldValuesAsText.Comments;
var tempLikes = pagecomments[0].FieldValuesAsText.Likes;
if (tempLikes != undefined && tempLikes != null && tempLikes !== "") tempLikes = JSON.parse(tempLikes);
else tempLikes = [];
if (tempComments != undefined && tempComments != null && tempComments !== "") {
var jsonComments = JSON.parse(tempComments);
if (tempLikes.length > 0) {
tempLikes.map((liked) => {
var fil = _.find(jsonComments, (o) => { return o.id == liked.commentID; });
if (fil !== undefined && fil !== null) {
fil.upvote_count = liked.userVote.length;
var cufil = _.find(liked.userVote, (o) => { return o.userid == currentUserInfo.ID; });
if (cufil !== undefined && cufil !== null) fil.user_has_upvoted = true;
}
});
}
return jsonComments;
} else return [];
} else return [];
}
public getComment = async (pageurl) => {
let pagecomments = await this._list.items.select('Comments', 'FieldValuesAsText/Comments')
.filter(`PageURL eq '${pageurl}'`).expand('FieldValuesAsText').get();
if (pagecomments.length > 0) return pagecomments[0].FieldValuesAsText.Comments;
else return null;
}
public addComment = async (pageUrl, comments) => {
let pageName = pageUrl.split('/')[pageUrl.split('/').length - 1];
let commentsToAdd = await sp.web.lists.getByTitle(this.lst_pageComments).items.add({
Title: pageName,
PageURL: pageUrl,
Comments: JSON.stringify(comments)
});
return commentsToAdd;
}
public updateComment = async (pageurl, comments) => {
let pageComment = await this._list.items.select('ID', 'PageURL').filter(`PageURL eq '${pageurl}'`).get();
if (comments.length > 0) {
if (pageComment.length > 0) {
let pageCommentsToUpdate = await this._list.items.getById(pageComment[0].ID).update({
Comments: JSON.stringify(comments)
});
return pageCommentsToUpdate;
}
} else {
return await this._list.items.getById(pageComment[0].ID).delete();
}
}
public postComment = async (pageurl, commentJson, currentUserInfo) => {
commentJson.created_by_current_user = false;
let comments = await this.getPostComments(pageurl, currentUserInfo);
if (comments.length > 0) {
comments.push(commentJson);
let updateComments = await this.updateComment(pageurl, comments);
return updateComments;
} else {
comments.push(commentJson);
let addComments = await this.addComment(pageurl, comments);
return addComments;
}
}
public addVoteForComment = async (pageurl, commentJson, currentUserInfo) => {
var tempLikes = [];
tempLikes.push({
commentID: commentJson.id,
userVote: [{ userid: currentUserInfo.ID, name: currentUserInfo.DisplayName }]
});
let pageComment = await this._list.items.select('ID').filter(`PageURL eq '${pageurl}'`).get();
if (pageComment.length > 0) {
return await this._list.items.getById(pageComment[0].ID).update({ Likes: JSON.stringify(tempLikes) });
}
}
public updateVoteForComment = async (pageurl, jsonLikes) => {
let pageComment = await this._list.items.select('ID').filter(`PageURL eq '${pageurl}'`).get();
if (pageComment.length > 0) {
return await this._list.items.getById(pageComment[0].ID).update({ Likes: JSON.stringify(jsonLikes) });
}
}
public voteComment = async (pageurl, commentJson, currentUserInfo) => {
let res = await this._list.items.select('Likes', 'FieldValuesAsText/Likes').filter(`PageURL eq '${pageurl}'`).expand('FieldValuesAsText').get();
if (res.length > 0) {
var tempLikes = res[0].FieldValuesAsText.Likes;
if (tempLikes != undefined && tempLikes != null && tempLikes !== "") {
// Likes already exits so update the item
var jsonLikes = JSON.parse(tempLikes);
var userAlreadyVoted = _.find(jsonLikes, (o) => { return o.commentID == commentJson.id && _.find(o.userVote, (oo) => { return oo.userid == currentUserInfo.ID; }); });
var userPresent = (userAlreadyVoted === undefined || userAlreadyVoted == null) ? false : true;
var fil = _.find(jsonLikes, (o) => { return o.commentID == commentJson.id; });
if (fil !== undefined && fil !== null) {
// Found likes for the comment id
if (commentJson.user_has_upvoted) {
if (!userPresent) fil.userVote = _.concat(fil.userVote, { userid: currentUserInfo.ID, name: currentUserInfo.DisplayName });
} else {
if (userPresent) {
if (fil !== undefined && fil !== null) _.remove(fil.userVote, (o) => { return o['userid'] == currentUserInfo.ID; });
}
}
} else {
// No likes found for the comment id
jsonLikes.push({ commentID: commentJson.id, userVote: [{ userid: currentUserInfo.ID, name: currentUserInfo.DisplayName }] });
}
return await this.updateVoteForComment(pageurl, jsonLikes);
} else {
// Likes doesn't exists so add new
if (commentJson.user_has_upvoted) return await this.addVoteForComment(pageurl, commentJson, currentUserInfo);
}
} else {
return commentJson;
}
}
public deleteComment = async (pageurl, commentJson) => {
let comments = await this.getComment(pageurl);
if (comments !== undefined && comments !== null) {
var jsonComments = JSON.parse(comments);
_.remove(jsonComments, (o) => { return o['id'] == commentJson.id; });
return await this.updateComment(pageurl, jsonComments);
}
}
public editComments = async (pageurl, commentJson) => {
let comment = await this.getComment(pageurl);
if (comment !== undefined && comment !== null) {
var jsonComments = JSON.parse(comment);
var match = _.find(jsonComments, (o) => { return o.id == commentJson.id; });
if (match) _.merge(match, { pings: commentJson.pings, content: commentJson.content, modified: commentJson.modified });
return await this.updateComment(pageurl, jsonComments);
}
}
public createFolder = async (folderPath) => {
return await sp.web.folders.add(folderPath);
}
public uploadFileToFolder = async (folderpath, fileinfo) => {
return await sp.web.getFolderByServerRelativeUrl(folderpath).files.add(fileinfo.name, fileinfo.content, true);
}
public postAttachments = async (commentArray: any[], pageFolderExists, postAttachmentPath): Promise<any> => {
var self = this;
return new Promise(async (resolve, reject) => {
if (!pageFolderExists) await this.createFolder(postAttachmentPath);
var reader = new FileReader();
reader.onload = async () => {
var contentBuffer = reader.result;
let uploadedFile = await self.uploadFileToFolder(postAttachmentPath, { name: commentArray[0].file.name, content: contentBuffer });
_.set(commentArray[0], 'file_id', uploadedFile.data.UniqueId);
_.set(commentArray[0], 'file_url', postAttachmentPath + "/" + commentArray[0].file.name);
resolve(commentArray);
};
await reader.readAsArrayBuffer(commentArray[0].file);
});
}
public checkListExists = async (): Promise<boolean> => {
return new Promise<boolean>(async (res, rej) => {
sp.web.lists.getByTitle(this.lst_pageComments).get().then((listExists) => {
res(true);
}).catch(async err => {
let listExists = await (await sp.web.lists.ensure(this.lst_pageComments)).list;
await listExists.fields.addText('PageURL', 255, { Required: true, Description: '' });
await listExists.fields.addMultilineText('Comments', 6, false, false, false, false, { Required: true, Description: '' });
await listExists.fields.addMultilineText('Likes', 6, false, false, false, false, { Required: false, Description: '' });
let allItemsView = await listExists.views.getByTitle('All Items');
await allItemsView.fields.add('PageURL');
await allItemsView.fields.add('Comments');
await allItemsView.fields.add('Likes');
res(true);
});
});
}
}

View File

@ -0,0 +1,789 @@
/*jquery-comments.js 1.4.0
(c) 2017 Joona Tykkyläinen, Viima Solutions Oy
jquery-comments may be freely distributed under the MIT license.
For all details and documentation:
http://viima.github.io/jquery-comments/*/
.jquery-comments * {
box-sizing: border-box;
text-shadow: none;
}
.jquery-comments a[href] {
color: #2793e6;
text-decoration: none;
font-size: 13px;
font-weight: normal;
}
.jquery-comments a[href]:hover {
text-decoration: none;
font-weight: bold;
}
.jquery-comments a[href]:visited {
text-decoration: none;
color: #2793e6;
}
.jquery-comments .textarea, .jquery-comments input, .jquery-comments button {
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
vertical-align: top;
border-radius: 0;
margin: 0;
padding: 0;
border: 0;
outline: 0;
background: rgba(0, 0, 0, 0);
}
.jquery-comments button {
vertical-align: inherit;
}
.jquery-comments .tag {
color: inherit;
font-size: 0.9em;
line-height: 1.2em;
background: #ddd;
border: 1px solid #ccc;
padding: 0.1em 0.3em;
cursor: pointer;
font-weight: normal;
border-radius: 1em;
transition: all 0.2s linear;
}
.jquery-comments .tag:hover {
text-decoration: none;
background-color: #d8edf8;
border-color: #2793e6;
}
.jquery-comments [contentEditable=true]:empty:not(:focus):before{
content:attr(data-placeholder);
color: #CCC;
position: inherit;
}
.jquery-comments i.fa {
width: 1em;
height: 1em;
background-size: cover;
}
.jquery-comments i.fa.image:before {
content: "";
}
.jquery-comments .spinner {
font-size: 2em;
text-align: center;
padding: 0.5em;
color: #666;
}
.jquery-comments ul {
list-style: none;
padding: 0;
margin: 0;
}
.jquery-comments .profile-picture {
float: left;
width: 2.5rem;
height: 2.5rem;
max-width: 50px;
max-height: 50px;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
}
.jquery-comments i.profile-picture {
font-size: 3.4em;
text-align: center;
}
.jquery-comments .profile-picture.round {
border-radius: 50%;
}
.jquery-comments .commenting-field.main{
margin-bottom: 0.75em;
}
.jquery-comments .commenting-field.main .profile-picture {
margin-bottom: 0.5rem;
}
.jquery-comments .textarea-wrapper {
overflow: hidden;
padding-left: 15px;
position: relative;
}
.jquery-comments .textarea-wrapper:before {
content: " ";
position: absolute;
border: 5px solid #D5D5D5;
left: 5px;
top: 0;
width: 10px;
height: 10px;
box-sizing: border-box;
border-bottom-color: rgba(0, 0, 0, 0);
border-left-color: rgba(0, 0, 0, 0);
}
.jquery-comments .textarea-wrapper:after {
content: " ";
position: absolute;
border: 7px solid #FFF;
left: 7px;
top: 1px;
width: 10px;
height: 10px;
box-sizing: border-box;
border-bottom-color: rgba(0, 0, 0, 0);
border-left-color: rgba(0, 0, 0, 0);
}
.jquery-comments .textarea-wrapper .inline-button {
cursor: pointer;
right: 0;
z-index: 10;
position: absolute;
border: .5em solid rgba(0,0,0,0);
box-sizing: content-box;
font-size: inherit;
overflow: hidden;
opacity: 0.5;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.jquery-comments .textarea-wrapper .inline-button:hover {
opacity: 1;
}
.jquery-comments:not(.mobile) .commenting-field-scrollable .textarea-wrapper .inline-button {
margin-right: 15px; /* Because of scrollbar */
}
.jquery-comments .textarea-wrapper .upload.inline-button i {
font-size: 1.3em;
}
.jquery-comments .textarea-wrapper .upload input {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
min-width: 100%;
height: 100%;
margin: 0;
padding: 0;
opacity: 0;
}
.jquery-comments .textarea-wrapper .close {
width: 1em;
height: 1em;
}
.jquery-comments .textarea-wrapper .close span {
background: #999;
width: 25%;
left: 37.5%;
height: 100%;
position: absolute;
-ms-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.jquery-comments .textarea-wrapper .close .right {
-ms-transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.jquery-comments .textarea-wrapper .textarea {
margin: 0;
outline: 0;
overflow-y: auto;
overflow-x: hidden;
cursor: text;
border: 1px solid #CCC;;
background: #FFF;
font-size: 1em;
line-height: 1.45em;
padding: .25em .8em;
padding-right: 2em;
}
.jquery-comments:not(.mobile) .commenting-field-scrollable .textarea-wrapper .textarea {
padding-right: calc(2em + 15px); /* Because of scrollbar */
}
.jquery-comments .textarea-wrapper .control-row > span {
float: right;
color: #FFF;
padding: 0 1em;
font-size: 1em;
line-height: 1.6em;
margin-top: .4em;
border: 1px solid rgba(0, 0, 0, 0);
opacity: .5;
}
.jquery-comments .textarea-wrapper .control-row > span:not(:first-child) {
margin-right: .5em;
}
.jquery-comments .textarea-wrapper .control-row > span.enabled {
opacity: 1;
cursor: pointer;
}
.jquery-comments .textarea-wrapper .control-row > span:not(.enabled) {
pointer-events: none;
}
.jquery-comments .textarea-wrapper .control-row > span.enabled:hover {
opacity: .9;
}
.jquery-comments .textarea-wrapper .control-row > span.upload {
position: relative;
overflow: hidden;
background-color: #999;
}
.jquery-comments ul.navigation {
clear: both;
color: #CCC;
border-bottom: 2px solid #CCC;
line-height: 2em;
font-size: 1em;
margin-bottom: 0.5em;
}
.jquery-comments ul.navigation .navigation-wrapper {
position: relative;
}
.jquery-comments ul.navigation li {
display: inline-block;
position: relative;
padding: 0 1em;
cursor: pointer;
font-weight: bold;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.jquery-comments ul.navigation li.active,
.jquery-comments ul.navigation li:hover {
color: #000;
}
.jquery-comments ul.navigation li.active:after {
content: " ";
display: block;
right: 0;
height: 2px;
background: #000;
position: absolute;
bottom: -2px;
left: 0;
}
.jquery-comments ul.navigation li[data-sort-key="attachments"] {
float: right;
}
.jquery-comments ul.navigation li[data-sort-key="attachments"] i {
margin-right: 0.25em;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive {
display: none;
}
@media screen and (max-width: 600px) {
.jquery-comments ul.navigation .navigation-wrapper {
display: none;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive {
display: inline;
}
}
.jquery-comments.responsive ul.navigation .navigation-wrapper {
display: none;
}
.jquery-comments.responsive ul.navigation .navigation-wrapper.responsive {
display: inline;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive li.title {
padding: 0 1.5em;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive li.title header:after {
display: inline-block;
content: "";
border-left: 0.3em solid rgba(0, 0, 0, 0) !important;
border-right: 0.3em solid rgba(0, 0, 0, 0) !important;
border-top: 0.4em solid #CCC;
margin-left: 0.5em;
position: relative;
top: -0.1em;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive li.title.active header:after,
.jquery-comments ul.navigation .navigation-wrapper.responsive li.title:hover header:after {
border-top-color: #000;
}
.jquery-comments ul.dropdown {
display: none;
position: absolute;
background: #FFF;
z-index: 99;
line-height: 1.2em;
border: 1px solid #CCC;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-ms-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
.jquery-comments ul.dropdown.autocomplete {
margin-top: 0.25em;
}
.jquery-comments ul.dropdown li {
display: block;
white-space: nowrap;
clear: both;
padding: 0.6em;
font-weight: normal;
cursor: pointer;
}
.jquery-comments ul.dropdown li.active {
background: #EEE;
}
.jquery-comments ul.dropdown li a {
display: block;
text-decoration: none;
color: inherit;
}
.jquery-comments ul.dropdown li .profile-picture {
float: left;
width: 2.4em;
height: 2.4em;
margin-right: 0.5em;
}
.jquery-comments ul.dropdown li .details {
display: inline-block;
}
.jquery-comments ul.dropdown li .name {
font-weight: bold;
}
.jquery-comments ul.dropdown li .details.no-email {
line-height: 2.4em;
}
.jquery-comments ul.dropdown li .email {
color: #999;
font-size: 0.95em;
margin-top: 0.1em;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive ul.dropdown {
left: 0;
width: 100%;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive ul.dropdown li {
color: #000;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive ul.dropdown li.active {
color: #FFF;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive ul.dropdown li:hover:not(.active) {
background: #F5F5F5;
}
.jquery-comments ul.navigation .navigation-wrapper.responsive ul.dropdown li:after {
display: none;
}
.jquery-comments .no-data {
display: none;
margin: 1em;
text-align: center;
font-size: 1.5em;
color: #CCC;
}
.jquery-comments ul.main:empty ~ .no-comments {
display: inherit;
}
.jquery-comments ul#attachment-list:empty ~ .no-attachments {
display: inherit;
}
.jquery-comments ul.main li.comment {
clear: both;
}
.jquery-comments ul.main li.comment .comment-wrapper,
.jquery-comments ul.main li.toggle-all,
.jquery-comments ul.main li.comment .commenting-field {
padding: .5em;
}
.jquery-comments ul.main li.comment .comment-wrapper {
border-top: 1px solid #DDD;
overflow: hidden;
}
.jquery-comments ul.main > li.comment:first-child > .comment-wrapper {
border-top: none;
}
.jquery-comments ul.main li.comment .comment-wrapper > .profile-picture {
margin-right: 1rem;
}
.jquery-comments ul.main li.comment time {
float: right;
line-height: 1.4em;
margin-left: .5em;
font-size: 0.8em;
color: #666;
}
.jquery-comments ul.main li.comment .name {
line-height: 1.4em;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-bottom: 5px;
}
.jquery-comments ul.main li.comment .name a {
color: inherit;
}
.jquery-comments ul.main li.comment .name .reply-to {
color: #999;
font-size: .8em;
font-weight: normal;
vertical-align: top;
}
.jquery-comments ul.main li.comment .name .reply-to i {
margin-left: .5em;
margin-right: .25em;
}
.jquery-comments ul.main li.comment .name .new {
margin-left: .5em;
background: #2793e6;
font-size: 0.8em;
padding: 0.2em 0.5em;
color: #fff;
font-weight: normal;
border-radius: 1em;
vertical-align: bottom;
}
.jquery-comments ul.main li.comment .wrapper{
line-height: 1.4em;
overflow: hidden;
}
.jquery-comments.mobile ul.main li.comment .child-comments li.comment .wrapper{
overflow: visible;
}
/* Content */
.jquery-comments ul.main li.comment .wrapper .content {
white-space: pre-line;
word-break: break-word;
}
.jquery-comments ul.main li.comment .wrapper .content a.attachment i {
margin-right: 0.5em;
}
.jquery-comments ul.main li.comment .wrapper .content a.attachment > * {
max-width: 100%;
max-height: 300px;
width: auto;
height: auto;
margin-top: .25em;
margin-bottom: .25em;
}
.jquery-comments ul.main li.comment .wrapper .content time.edited {
float: inherit;
margin: 0;
font-size: .9em;
font-style: italic;
color: #999;
}
.jquery-comments ul.main li.comment .wrapper .content time.edited:before {
content: " - ";
}
/* Actions */
.jquery-comments.mobile ul.main li.comment .actions {
font-size: 1em;
}
.jquery-comments ul.main li.comment .actions > * {
color: #999;
font-weight: bold;
}
.jquery-comments ul.main li.comment .actions .action {
display: inline-block;
cursor: pointer;
margin-left: 1em;
margin-right: 1em;
line-height: 1.5em;
font-size: 0.9em;
}
.jquery-comments ul.main li.comment .actions .action:first-child {
margin-left: 0;
}
.jquery-comments ul.main li.comment .actions .action.upvote {
cursor: inherit;
}
.jquery-comments ul.main li.comment .actions .action.upvote .upvote-count {
margin-right: .5em;
}
.jquery-comments ul.main li.comment .actions .action.upvote .upvote-count:empty {
display: none;
}
.jquery-comments ul.main li.comment .actions .action.upvote i {
cursor: pointer;
}
.jquery-comments ul.main li.comment .actions .action:not(.upvote):hover,
.jquery-comments ul.main li.comment .actions .action.upvote:not(.highlight-font) i:hover {
color: #666;
}
.jquery-comments ul.main li.comment .actions .action.delete {
opacity: 0.5;
pointer-events: none;
}
.jquery-comments ul.main li.comment .actions .action.delete.enabled {
opacity: 1;
pointer-events: auto;
}
.jquery-comments ul#attachment-list li.comment .actions .action:not(.delete) {
display: none;
}
.jquery-comments ul#attachment-list li.comment .actions .action.delete {
margin: 0;
}
.jquery-comments ul#attachment-list li.comment .actions .separator {
display: none;
}
/* Child comments */
.jquery-comments ul.main li.comment .child-comments > *:before { /* Margin for second level content */
content: "";
height: 1px;
float: left;
width: calc(3.6em + .5em); /* Profile picture width plus margin */
max-width: calc(50px + .5em); /* Profile picture max width plus margin */
}
.jquery-comments ul.main li.comment .child-comments .profile-picture {
width: 2.4rem;
height: 2.4rem;
}
.jquery-comments ul.main li.comment .child-comments i.profile-picture {
font-size: 2.4em;
}
.jquery-comments ul.main li.comment .child-comments li.toggle-all {
padding-top: 0;
}
.jquery-comments ul.main li.comment .child-comments li.toggle-all span:first-child {
vertical-align: middle;
}
.jquery-comments ul.main li.comment .child-comments li.toggle-all span:first-child:hover {
cursor: pointer;
text-decoration: underline;
}
.jquery-comments ul.main li.comment .child-comments li.toggle-all .caret {
display: inline-block;
vertical-align: middle;
width: 0;
height: 0;
margin-left: .5em;
border: .3em solid;
margin-top: .35em;
border-left-color: rgba(0, 0, 0, 0);
border-bottom-color: rgba(0, 0, 0, 0);
border-right-color: rgba(0, 0, 0, 0);
}
.jquery-comments ul.main li.comment .child-comments li.toggle-all .caret.up {
border-top-color: rgba(0, 0, 0, 0);
border-bottom-color: inherit;
margin-top: -.2em;
}
.jquery-comments ul.main li.comment .child-comments .togglable-reply {
display: none;
}
.jquery-comments ul.main li.comment .child-comments .visible {
display: inherit;
}
.jquery-comments ul.main li.comment.hidden {
display: none;
}
/* Editing comment */
.jquery-comments ul.main li.comment.edit > .comment-wrapper > *:not(.commenting-field) {
display: none;
}
.jquery-comments ul.main li.comment.edit > .comment-wrapper .commenting-field {
padding: 0 !important; /* Parent element has the padding */
}
/* Drag & drop attachments */
.jquery-comments.drag-ongoing {
overflow-y: hidden !important;
}
.jquery-comments .droppable-overlay {
display: table;
position: fixed;
z-index: 99;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.3)
}
.jquery-comments .droppable-overlay .droppable-container {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.jquery-comments .droppable-overlay .droppable-container .droppable {
background: #FFF;
color: #CCC;
padding: 6em;
}
.jquery-comments .droppable-overlay .droppable-container .droppable.drag-over {
color: #999;
}
.jquery-comments .droppable-overlay .droppable-container .droppable i {
margin-bottom: 5px;
}
/* Read-only mode */
.jquery-comments.read-only .commenting-field {
display: none;
}
.jquery-comments.read-only .actions {
display: none;
}
.displayctrl {
display: '' !important;
}
.hidectrl {
display: none !important;
}
.greenColor {
color: green;
}
.blueColor {
color: blue;
}
.redColor {
color: red;
}
.yellowColor {
color: yellow;
}
.jquery-comments .msgContainer {
margin-top: 5px;
display: inline-block;
}
.jquery-comments .errorMsg {
background: #f3c4c4;
}
.jquery-comments .msgAlert {
padding: 10px;
font-size: 12px;
font-weight: 500;
width: 100%;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
define([], function () {
return {
"PropertyPaneDescription": "",
"BasicGroupName": "Settings",
DateTimeFormatLabel: "Datetime format",
DateTimeFormatDescription: "Please use the moment datetime format option.",
RoundProfilePicLabel: "Profile Picture rounded",
RoundProfilePicDescription: "Enable to display the user profile picture as rounded.",
NavigationLabel: "Navigation",
NavigationDescription: "Tabs like Newest, Oldest, Popular & Attachments will be displayed.",
AttachmentLabel: "Attachments",
AttachmentDescription: "Allow the users to upload files.",
PingLabel: "Ping users",
PingDescription: "Allow users to ping other users in the comments.",
EditingLabel: "Edit Comments",
EditingDescription: "Allow the users to edit their own comments.",
UpVotingLabel: "Upvoting of comments",
UpVotingDescription: "Allow the users to upvote the comments.",
ReplyLabel: "Reply to comments",
ReplyDescription: "Allow the users to reply to the comments.",
DeleteLabel: "Delete own comments",
DeleteDescription: "Allow the users to delete their own comments.",
DeleteRepliesLabel: "Delete own comments with replies",
DeleteRepliesDescription: "Allow the users to delete their own comments with replies.",
HashtagsLabel: "Hashtags",
HashtagsDescription: "Allow the users to use hashtags.",
DocumentPreviewLabel: "Document Preview",
DocumentPreviewDescription: "Allow the users to view few of the document formats inline.",
AttachmentFileFormatLabel: "Allowed Files",
AttachmentFileFormatDescription: "Allowed files to attach. Please enter file extensions separated by ,",
AttachmentFileSizeLabel: "Max File Size",
AttachmentFileSizeDescription: "Maximum file size(MB) allowed to attach.",
AttachmentRepoLabel: "Select a libary",
AttachmentRepoPropValMsg: "Please choose the document library",
NoAttachmentRepoMsg: "Please select the document library to store the attachments.",
LoadingMsg: "Please wait..."
}
});

View File

@ -0,0 +1,42 @@
declare interface IPageCommentsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DateTimeFormatLabel: string;
DateTimeFormatDescription: string;
RoundProfilePicLabel: string;
RoundProfilePicDescription: string;
NavigationLabel: string;
NavigationDescription: string;
AttachmentLabel: string;
AttachmentDescription: string;
PingLabel: string;
PingDescription: string;
EditingLabel: string;
EditingDescription: string;
UpVotingLabel: string;
UpVotingDescription: string;
ReplyLabel: string;
ReplyDescription: string;
DeleteLabel: string;
DeleteDescription: string;
DeleteRepliesLabel: string;
DeleteRepliesDescription: string;
HashtagsLabel: string;
HashtagsDescription: string;
DocumentPreviewLabel: string;
DocumentPreviewDescription: string;
AttachmentFileFormatLabel: string;
AttachmentFileFormatDescription: string;
AttachmentFileSizeLabel: string;
AttachmentFileSizeDescription: string;
AttachmentRepoLabel: string;
AttachmentRepoPropValMsg: string;
NoAttachmentRepoMsg: string;
LoadingMsg: string;
}
declare module 'PageCommentsWebPartStrings' {
const strings: IPageCommentsWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

@ -3,9 +3,9 @@
"packageManager": "pnpm",
"isCreatingSolution": true,
"environment": "spo",
"version": "1.7.1",
"version": "1.10.0",
"libraryName": "workbench-customizer",
"libraryId": "5d6f4a5a-9d2b-4a93-a283-16b8f5ea75d6",
"componentType": "webpart"
}
}
}

View File

@ -4,13 +4,29 @@
This sample shows how the Workbench page can be customized to display in a way that better mimics a modern SharePoint page.
This is done using CSS overrides on some of the page styles, which does not cause any negative impact on your site as the web part is not intended to be consumed by final users, only developers.
The web part also has some properties that control which customizations are applied to the workbench page (all enabled by default).
The web part also has some properties that control which customizations are applied to the workbench page (all enabled by default). There is also an option to switch the page to Preview after the page is loaded, which gives a UI more close to what end users will see on a published page - this is very useful when doing UI work.
![Demo](./assets/Preview.png)
## Usage
### Deploy to tenant
The easiest way to use the solution is to package it up and deploy to the App Catalog. You can then add the web part to the bottom of the O365 Workbench page when developing your custom solutions.
### Run locally
Alternatively, you can add the output files for the web part to a custom SPFx project and the web part will also be served and available both from the local or hosted Workbench page:
* Generate the output files for the solution by executing `gulp bundle --ship`
* Copy all files from js-workbench-customizer\dist to the dist folder of your custom solution
* Copy the workbenchCustomizer folder from js-workbench-customizer\lib\webparts to the corresponding webparts folder of your custom solution
Note: This approach will not "pollute" your solution with additional resources or dependencies as the SPFx toolchain will ignore those additional files by default when you package your solution. You can also clean everything by running `gulp clean` as both the lib and dist folders are deleted and recreated again. Git will also ignore dist and lib folders by default, so the files will never be added to source control.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.7.1-green.svg)
![drop](https://img.shields.io/badge/drop-1.10.0-green.svg)
## Applies to
@ -32,6 +48,7 @@ workbench-customizer|Joel Rodrigues
Version|Date|Comments
-------|----|--------
1.0|January 24, 2019|Initial release
1.1|February 05, 2020|Update to SPFx 1.10.0
## Disclaimer

View File

@ -2,6 +2,7 @@
"name": "workbench-customizer",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
@ -11,22 +12,24 @@
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.7.1",
"@microsoft/sp-lodash-subset": "1.7.1",
"@microsoft/sp-office-ui-fabric-core": "1.7.1",
"@microsoft/sp-webpart-base": "1.7.1",
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@types/es6-promise": "0.0.33",
"@types/webpack-env": "1.13.1",
"npm": "^6.7.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.7.1",
"@microsoft/sp-tslint-rules": "1.7.1",
"@microsoft/sp-module-interfaces": "1.7.1",
"@microsoft/sp-webpart-workbench": "1.7.1",
"gulp": "~3.9.1",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
"supportedHosts": ["SharePointWebPart"],
// 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.

View File

@ -1,9 +1,6 @@
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { IPropertyPaneConfiguration, PropertyPaneToggle } from "@microsoft/sp-property-pane";
import styles from './WorkbenchCustomizerWebPart.module.scss';
import * as strings from 'WorkbenchCustomizerWebPartStrings';

View File

@ -1,4 +1,5 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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"

View File

@ -0,0 +1,338 @@
# Upgrade project C:\Users\joelf\dev\GitHub\sp-dev-fx-webparts\samples\js-workbench-customizer to v1.10.0
Date: 1/21/2020
## Findings
Following is the list of steps required to upgrade your project to SharePoint Framework version 1.10.0. [Summary](#Summary) of the modifications is included at the end of the report.
### FN001001 @microsoft/sp-core-library | Required
Upgrade SharePoint Framework dependency package @microsoft/sp-core-library
Execute the following command:
```sh
pnpm i -E @microsoft/sp-core-library@1.10.0
```
File: [./package.json](./package.json)
### FN001002 @microsoft/sp-lodash-subset | Required
Upgrade SharePoint Framework dependency package @microsoft/sp-lodash-subset
Execute the following command:
```sh
pnpm i -E @microsoft/sp-lodash-subset@1.10.0
```
File: [./package.json](./package.json)
### FN001003 @microsoft/sp-office-ui-fabric-core | Required
Upgrade SharePoint Framework dependency package @microsoft/sp-office-ui-fabric-core
Execute the following command:
```sh
pnpm i -E @microsoft/sp-office-ui-fabric-core@1.10.0
```
File: [./package.json](./package.json)
### FN001004 @microsoft/sp-webpart-base | Required
Upgrade SharePoint Framework dependency package @microsoft/sp-webpart-base
Execute the following command:
```sh
pnpm i -E @microsoft/sp-webpart-base@1.10.0
```
File: [./package.json](./package.json)
### FN001021 @microsoft/sp-property-pane | Required
Install SharePoint Framework dependency package @microsoft/sp-property-pane
Execute the following command:
```sh
pnpm i -E @microsoft/sp-property-pane@1.10.0
```
File: [./package.json](./package.json)
### FN002001 @microsoft/sp-build-web | Required
Upgrade SharePoint Framework dev dependency package @microsoft/sp-build-web
Execute the following command:
```sh
pnpm i -DE @microsoft/sp-build-web@1.10.0
```
File: [./package.json](./package.json)
### FN002002 @microsoft/sp-module-interfaces | Required
Upgrade SharePoint Framework dev dependency package @microsoft/sp-module-interfaces
Execute the following command:
```sh
pnpm i -DE @microsoft/sp-module-interfaces@1.10.0
```
File: [./package.json](./package.json)
### FN002003 @microsoft/sp-webpart-workbench | Required
Upgrade SharePoint Framework dev dependency package @microsoft/sp-webpart-workbench
Execute the following command:
```sh
pnpm i -DE @microsoft/sp-webpart-workbench@1.10.0
```
File: [./package.json](./package.json)
### FN002009 @microsoft/sp-tslint-rules | Required
Upgrade SharePoint Framework dev dependency package @microsoft/sp-tslint-rules
Execute the following command:
```sh
pnpm i -DE @microsoft/sp-tslint-rules@1.10.0
```
File: [./package.json](./package.json)
### FN002012 @microsoft/rush-stack-compiler-3.3 | Required
Install SharePoint Framework dev dependency package @microsoft/rush-stack-compiler-3.3
Execute the following command:
```sh
pnpm i -DE @microsoft/rush-stack-compiler-3.3@0.3.5
```
File: [./package.json](./package.json)
### FN010001 .yo-rc.json version | Recommended
Update version in .yo-rc.json
In file [./.yo-rc.json](./.yo-rc.json) update the code as follows:
```json
{
"@microsoft/generator-sharepoint": {
"version": "1.10.0"
}
}
```
File: [./.yo-rc.json](./.yo-rc.json)
### FN012017 tsconfig.json extends property | Required
Update tsconfig.json extends property
In file [./tsconfig.json](./tsconfig.json) update the code as follows:
```json
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json"
}
```
File: [./tsconfig.json](./tsconfig.json)
### FN016004 Property pane property import change to @microsoft/sp-property-pane | Required
Refactor the code to import property pane property from the @microsoft/sp-property-pane npm package instead of the @microsoft/sp-webpart-base package
In file [src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.ts](src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.ts) update the code as follows:
```ts
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { IPropertyPaneConfiguration, PropertyPaneToggle } from "@microsoft/sp-property-pane";
```
File: [src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.ts:2:1](src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.ts)
### FN021001 main | Required
Add package.json property
In file [./package.json](./package.json) update the code as follows:
```json
{
"main": "lib/index.js"
}
```
File: [./package.json](./package.json)
### FN011011 Web part manifest supportedHosts | Required
Update the supportedHosts property in the manifest
In file [src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.manifest.json](src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.manifest.json) update the code as follows:
```json
{
"supportedHosts": ["SharePointWebPart"]
}
```
File: [src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.manifest.json](src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.manifest.json)
### FN012014 tsconfig.json compiler options inlineSources | Required
Update tsconfig.json inlineSources value
In file [./tsconfig.json](./tsconfig.json) update the code as follows:
```json
{
"compilerOptions": {
"inlineSources": false
}
}
```
File: [./tsconfig.json](./tsconfig.json)
### FN012015 tsconfig.json compiler options strictNullChecks | Required
Update tsconfig.json strictNullChecks value
In file [./tsconfig.json](./tsconfig.json) update the code as follows:
```json
{
"compilerOptions": {
"strictNullChecks": false
}
}
```
File: [./tsconfig.json](./tsconfig.json)
### FN012016 tsconfig.json compiler options noUnusedLocals | Required
Update tsconfig.json noUnusedLocals value
In file [./tsconfig.json](./tsconfig.json) update the code as follows:
```json
{
"compilerOptions": {
"noUnusedLocals": false
}
}
```
File: [./tsconfig.json](./tsconfig.json)
## Summary
### Execute script
```sh
pnpm i -E @microsoft/sp-core-library@1.10.0 @microsoft/sp-lodash-subset@1.10.0 @microsoft/sp-office-ui-fabric-core@1.10.0 @microsoft/sp-webpart-base@1.10.0 @microsoft/sp-property-pane@1.10.0
pnpm i -DE @microsoft/sp-build-web@1.10.0 @microsoft/sp-module-interfaces@1.10.0 @microsoft/sp-webpart-workbench@1.10.0 @microsoft/sp-tslint-rules@1.10.0 @microsoft/rush-stack-compiler-3.3@0.3.5
```
### Modify files
#### [./.yo-rc.json](./.yo-rc.json)
Update version in .yo-rc.json:
```json
{
"@microsoft/generator-sharepoint": {
"version": "1.10.0"
}
}
```
#### [./tsconfig.json](./tsconfig.json)
Update tsconfig.json extends property:
```json
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json"
}
```
Update tsconfig.json inlineSources value:
```json
{
"compilerOptions": {
"inlineSources": false
}
}
```
Update tsconfig.json strictNullChecks value:
```json
{
"compilerOptions": {
"strictNullChecks": false
}
}
```
Update tsconfig.json noUnusedLocals value:
```json
{
"compilerOptions": {
"noUnusedLocals": false
}
}
```
#### [src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.ts](src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.ts)
Refactor the code to import property pane property from the @microsoft/sp-property-pane npm package instead of the @microsoft/sp-webpart-base package:
```ts
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { IPropertyPaneConfiguration, PropertyPaneToggle } from "@microsoft/sp-property-pane";
```
#### [./package.json](./package.json)
Add package.json property:
```json
{
"main": "lib/index.js"
}
```
#### [src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.manifest.json](src\webparts\workbenchCustomizer\WorkbenchCustomizerWebPart.manifest.json)
Update the supportedHosts property in the manifest:
```json
{
"supportedHosts": ["SharePointWebPart"]
}
```

View File

@ -1,8 +1,10 @@
{
"@microsoft/generator-sharepoint": {
"libraryName": "react-async-await-sp-pnp-js",
"framework": "react",
"version": "1.0.2",
"libraryId": "c3bd0bfa-1bee-4af0-ad3d-a72d02b4dc7c"
"version": "1.10.0",
"isDomainIsolated": false,
"isCreatingSolution": true,
"packageManager": "npm",
"componentType": "webpart",
"framework": ""
}
}
}

View File

@ -1,5 +1,5 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"async-await-pn-p-js-bundle": {

View File

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

View File

@ -1,4 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-async-await-sp-pnp-js",

View File

@ -1,8 +1,11 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-async-await-sp-pnp-js-client-side-solution",
"id": "c3bd0bfa-1bee-4af0-ad3d-a72d02b4dc7c",
"version": "1.0.0.0"
"version": "1.0.0.0",
"isDomainIsolated": false,
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/react-async-await-sp-pnp-js.sppkg"

View File

@ -1,4 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,

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,3 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,43 @@
{
"name": "react-async-await-sp-pnp-js",
"version": "0.0.1",
"version": "1.1.10",
"private": true,
"engines": {
"node": ">=0.10.0"
"node": ">=0.10.13"
},
"main": "lib/index.js",
"dependencies": {
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@microsoft/rush-stack-compiler-3.2": "^0.6.2",
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/common": "^1.1.2",
"@pnp/logging": "^1.1.2",
"@pnp/odata": "^1.1.2",
"@pnp/sp": "^1.1.2",
"@types/react": "0.14.46",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"react": "15.4.2",
"react-dom": "15.4.2"
"@pnp/sp": "^2.0.2",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"natives": "^1.1.6",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "5.2.2",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
"tslint-microsoft-contrib": "5.0.0"
},
"resolutions": {
"@types/react": "16.8.8"
},
"scripts": {
"build": "gulp bundle",

View File

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

View File

@ -1,15 +1,17 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "c81d0661-6ead-4c3c-a3a0-261e2a7edd44",
"alias": "AsyncAwaitPnPJsWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"supportedHosts": ["SharePointWebPart"],
"safeWithCustomScriptDisabled": false,
"version": "*",
"preconfiguredEntries": [{
"groupId": "c81d0661-6ead-4c3c-a3a0-261e2a7edd44",
"group": { "default": "Under Development" },
"group": { "default": "Other" },
"title": { "default": "AsyncAwaitPnPJs" },
"description": { "default": "AsyncAwaitPnPJs description" },
"officeFabricIconFontName": "Page",

View File

@ -1,11 +1,13 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
} from '@microsoft/sp-property-pane';
import { sp } from '@pnp/sp';
import * as strings from 'asyncAwaitPnPJsStrings';
import AsyncAwaitPnPJs from './components/AsyncAwaitPnPJs';
@ -17,14 +19,14 @@ import { IAsyncAwaitPnPJsWebPartProps } from './IAsyncAwaitPnPJsWebPartProps';
export default class AsyncAwaitPnPJsWebPart extends BaseClientSideWebPart<IAsyncAwaitPnPJsWebPartProps> {
// // https://github.com/SharePoint/PnP-JS-Core/wiki/Using-sp-pnp-js-in-SharePoint-Framework
// public onInit(): Promise<void> {
// return super.onInit().then(_ => {
// // establish SPFx context
// pnp.setup({
// spfxContext: this.context
// });
// });
// }
public onInit(): Promise<void> {
return super.onInit().then(_ => {
// establish SPFx context
sp.setup({
spfxContext: this.context
});
});
}
public render(): void {
const element: React.ReactElement<IAsyncAwaitPnPJsProps > = React.createElement(

View File

@ -1,4 +1,6 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.container {
max-width: 700px;
margin: 0px auto;

View File

@ -5,7 +5,10 @@ import styles from "./AsyncAwaitPnPJs.module.scss";
import { IFile, IResponseItem } from "../interfaces";
// import pnp and pnp logging system
import { Web } from "@pnp/sp";
import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { Logger, LogLevel, LogEntry, FunctionListener } from "@pnp/logging";
// import SPFx Logging system
@ -139,8 +142,8 @@ export default class AsyncAwaitPnPJs extends React.Component<IAsyncAwaitPnPJsPro
// - .usingCaching() will be using SessionStorage by default to cache the results
// - .get() always returns a promise
// - await converts Promise<IResponseItem[]> into IResponse[]
const web: Web = new Web(this.props.pageContext.web.absoluteUrl);
const response: IResponseItem[] = await web.lists
const response: IResponseItem[] = await sp.web.lists
.getByTitle(libraryName)
.items
.select("Title", "FileLeafRef", "File/Length")
@ -161,10 +164,10 @@ export default class AsyncAwaitPnPJs extends React.Component<IAsyncAwaitPnPJsPro
this.setState({ ...this.state, items });
// intentionally set wrong query to see console errors...
const failResponse: IResponseItem[] = await web.lists
const failResponse: IResponseItem[] = await sp.web.lists
.getByTitle(libraryName)
.items
.select("Title", "FileLeafRef", "File/Length", "NonExistingColumn")
.select("Title", "FileLeafRef", "File/Length")
.expand("File/Length")
.usingCaching()
.get();

View File

@ -0,0 +1,46 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.2/MicrosoftTeams.schema.json",
"manifestVersion": "1.2",
"packageName": "AsyncAwaitPnPJs",
"id": "c81d0661-6ead-4c3c-a3a0-261e2a7edd44",
"version": "0.1",
"developer": {
"name": "SPFx + Teams Dev",
"websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration",
"privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement",
"termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement"
},
"name": {
"short": "AsyncAwaitPnPJs"
},
"description": {
"short": "AsyncAwaitPnPJs description",
"full": "AsyncAwaitPnPJs description"
},
"icons": {
"outline": "tab20x20.png",
"color": "tab96x96.png"
},
"accentColor": "#004578",
"configurableTabs": [
{
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=c81d0661-6ead-4c3c-a3a0-261e2a7edd44", "canUpdateConfiguration": true,
"scopes": [
"team"
]
}
],
"validDomains": [
"*.login.microsoftonline.com",
"*.sharepoint.com",
"*.sharepoint-df.com",
"spoppe-a.akamaihd.net",
"spoprod-a.akamaihd.net",
"resourceseng.blob.core.windows.net",
"msft.spoppe.com"
],
"webApplicationInfo": {
"resource": "https://{teamSiteDomain}",
"id": "00000003-0000-0ff1-ce00-000000000000"
}
}

View File

@ -1,13 +1,18 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
@ -21,5 +26,12 @@
"dom",
"es2015.collection"
]
}
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

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

View File

@ -1,8 +0,0 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;

View File

@ -1 +0,0 @@
/// <reference path="@ms/odsp.d.ts" />

View File

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

View File

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

View File

@ -0,0 +1,13 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": true,
"isCreatingSolution": true,
"environment": "spo",
"version": "1.10.0",
"libraryName": "outlook-2-sp-spfx",
"libraryId": "41e21307-ed8c-4409-b12f-9c575675bb37",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,72 @@
## outlook-2-teams-spfx
## Summary
This SPFx Outlook Add-In lets you browse your OneDrive, joined Teams or Groups and select a folder to save your complete mail in there.
This sample shows you working with the current Office context and receive information on currently selected mail from there.
Furthermore it shows you how to retrieve a complete mail as a mimestream via Microsoft Graph and finally two file operations with Microsoft Graph as well:
* Writing normal files smaller 4MB
* Writing big files with an UploadSession when bigger than 4MB
## outlook-2-teams-spfx in action
![WebPartInAction](https://mmsharepoint.files.wordpress.com/2020/01/addin_overall.png)
A detailed functionality and technical description can be found in the [author's blog series](https://mmsharepoint.wordpress.com/2020/01/11/an-outlook-add-in-with-sharepoint-framework-spfx-introduction/)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.10.0-green.svg)
## Applies to
* [Tutorial for creating Outlook Web Access extension using SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/office-addins-tutorial)
## Solution
Solution|Author(s)
--------|---------
outlook-2-teams-spfx| Markus Moeller ([@moeller2_0](http://www.twitter.com/moeller2_0))
## Version history
Version|Date|Comments
-------|----|--------
1.0|February 05, 2020|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:
* restore dependencies: `npm install`
From here you can also follow the deployment steps from the official [Microsoft Tutorial](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/office-addins-tutorial#packaging-and-deploying-your-solution-to-sharepoint)
* build solution `gulp build --ship`
* bundle solution: `gulp bundle --ship`
* package solution: `gulp package-solution --ship`
* locate solution at `.\sharepoint\solution\outlook-2-sp-spfx.sppkg`
* upload it to your tenant app catalog
* Go to your Outlook Web Access and selct a mail
* Choose ... and "Get Add Ins"
* Choose My Add-ins from left menu
* Choose *Add from file... under the Custom add-ins
* Upload the manifest xml file from \officeAddin folder
* Click Install on the warning message to get your add-in available on the tenant
* Close the add-in window by clicking X on the top-right corner
* Activate again the context menu from [...] and choose "Copy to SharePoint" to activate the add-in in your inbox
## Features
This Outlook Add-In shows the following capabilities on top of the SharePoint Framework:
* Select Office context and attributes of currently selected mail
* Use Microsoft Graph to retrieve joined Groups and Teams
* Use Microsoft Graph to retrieve folders and subfolders for OneDrive or Teams/Group drives
* Use Microsoft Graph to retrieve complete mail mimestream by given ID
* Use Microsoft Graph to save normal or big files (in size bigger 4MB) with different concepts
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-outlook-copy2teams" />

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"outlook-2-share-point-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/outlook2SharePoint/Outlook2SharePointWebPart.js",
"manifest": "./src/webparts/outlook2SharePoint/Outlook2SharePointWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"Outlook2SharePointWebPartStrings": "lib/webparts/outlook2SharePoint/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "outlook-2-sp-spfx-client-side-solution",
"id": "41e21307-ed8c-4409-b12f-9c575675bb37",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "Group.Read.All"
},
{
"resource": "Windows Azure Active Directory",
"scope": "User.Read"
},
{
"resource": "Microsoft Graph",
"scope": "Files.Read"
},
{
"resource": "Microsoft Graph",
"scope": "Files.ReadWrite"
},
{
"resource": "Microsoft Graph",
"scope": "Mail.Read"
},
{
"resource": "Microsoft Graph",
"scope": "Sites.ReadWrite.All"
}
]
},
"paths": {
"zippedPackage": "solution/outlook-2-sp-spfx.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

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

View File

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

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0"
xsi:type="MailApp">
<Id>7f1ef545-1d02-4cbd-b4e1-2f4140c1667a</Id>
<Version>1.0.0.0</Version>
<ProviderName>SPFx Provider</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Copy to SharePoint"/>
<Description DefaultValue="An Add-In to copy full mails to Teams, Groups or OneDrive."/>
<IconUrl DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
<HighResolutionIconUrl DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
<SupportUrl DefaultValue="https://localhost:4321/help"/>
<AppDomains>
<AppDomain>https://login.microsoftonline.com</AppDomain>
<AppDomain>https://login.windows.net</AppDomain>
</AppDomains>
<Hosts>
<Host Name="Mailbox" />
</Hosts>
<Requirements>
<Sets>
<Set Name="Mailbox" MinVersion="1.4" />
<Set Name="SharePointHostedAddin" MinVersion="1.1" />
</Sets>
</Requirements>
<FormSettings>
<Form xsi:type="ItemRead">
<DesktopSettings>
<SourceLocation DefaultValue="https://_SharePointTenantUrl_/_layouts/15/outlookhostedapp.aspx?componentId=7f1ef545-1d02-4cbd-b4e1-2f4140c1667a&amp;isConfigureMode=true"/>
<RequestedHeight>250</RequestedHeight>
</DesktopSettings>
</Form>
</FormSettings>
<Permissions>ReadWriteMailbox</Permissions>
<Rule xsi:type="RuleCollection" Mode="Or">
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Read" />
</Rule>
<DisableEntityHighlighting>false</DisableEntityHighlighting>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides" xsi:type="VersionOverridesV1_0">
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
<Hosts>
<Host xsi:type="MailHost">
<DesktopFormFactor>
<ExtensionPoint xsi:type="MessageReadCommandSurface">
<OfficeTab id="TabDefault">
<Group id="msgReadGroup">
<Label resid="GroupLabel" />
<Control xsi:type="Button" id="msgReadOpenPaneButton">
<Label resid="TaskpaneButton.Label" />
<Supertip>
<Title resid="TaskpaneButton.Label" />
<Description resid="TaskpaneButton.Tooltip" />
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16" />
<bt:Image size="32" resid="Icon.32x32" />
<bt:Image size="80" resid="Icon.80x80" />
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url" />
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
</DesktopFormFactor>
</Host>
</Hosts>
<Resources>
<bt:Images>
<bt:Image id="Icon.16x16" DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
<bt:Image id="Icon.32x32" DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
<bt:Image id="Icon.80x80" DefaultValue="https://cdn.graph.office.net/prod/media/shared/addin-icon.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="Taskpane.Url" DefaultValue="https://_SharePointTenantUrl_/_layouts/15/outlookhostedapp.aspx?componentId=7f1ef545-1d02-4cbd-b4e1-2f4140c1667a&amp;isConfigureMode=true" />
</bt:Urls>
<bt:ShortStrings>
<bt:String id="GroupLabel" DefaultValue="Add-in groupLabel"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Show Taskpane"/>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Opens taskpane."/>
</bt:LongStrings>
</Resources>
</VersionOverrides>
</VersionOverrides>
</OfficeApp>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "outlook-2-sp-spfx",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.10.0-plusbeta",
"@microsoft/sp-lodash-subset": "1.10.0-plusbeta",
"@microsoft/sp-office-ui-fabric-core": "1.10.0-plusbeta",
"@microsoft/sp-property-pane": "1.10.0-plusbeta",
"@microsoft/sp-webpart-base": "1.10.0-plusbeta",
"@types/es6-promise": "0.0.33",
"@types/office-js": "^1.0.59",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0-plusbeta",
"@microsoft/sp-tslint-rules": "1.10.0-plusbeta",
"@microsoft/sp-module-interfaces": "1.10.0-plusbeta",
"@microsoft/sp-webpart-workbench": "1.10.0-plusbeta",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,221 @@
import { MSGraphClient, MSGraphClientFactory } from '@microsoft/sp-http';
import Utilities from './Utilities';
import { IFolder } from '../model/IFolder';
import { IMail } from '../model/IMail';
export default class GraphController {
private client: MSGraphClient;
constructor (graphFactory: MSGraphClientFactory, callback: () => void) {
graphFactory
.getClient()
.then((client: MSGraphClient) => {
this.client = client;
callback();
});
this.retrieveMimeMail = this.retrieveMimeMail.bind(this);
}
public getClient() {
return this.client;
}
/**
* This function retrieves all 1st-level folders from user's OneDrive
*/
public getOneDriveFolder(): Promise<IFolder[]> {
return this.client
.api('me/drive/root/children')
.version('v1.0')
.filter('folder ne null')
.get()
.then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: item.parentReference.driveId, parentFolder: null});
});
return folders;
});
}
public getGroupRootFolders(group: IFolder): Promise<IFolder[]> {
return this.client
.api(`drives/${group.driveID}/root/children`)
.version('v1.0')
.filter('folder ne null')
.get()
.then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: group.driveID, parentFolder: group});
});
return folders;
});
}
public getSubFolder(folder: IFolder): Promise<IFolder[]> {
return this.client
.api(`drives/${folder.driveID}/items/${folder.id}/children`)
.version('v1.0')
.filter('folder ne null')
.get()
.then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: folder.driveID, parentFolder: folder});
});
return folders;
});
}
/**
* This function retrievs the user's membership groups from Graph
*/
public getJoinedGroups(): Promise<IFolder[]> {
return this.client
.api('me/memberOf')
.version('v1.0')
.get()
.then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => {
// Show unified Groups but NO Teams
if (item['@odata.type'] === '#microsoft.graph.group') {
if(!item.resourceProvisioningOptions || item.resourceProvisioningOptions.indexOf('Team') === -1) {
folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null});
}
}
});
return folders;
});
}
/**
* This function retrievs the user's membership groups from Graph
*/
public getJoinedTeams(): Promise<IFolder[]> {
return this.client
.api('me/joinedTeams')
.version('v1.0')
.get()
.then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => {
folders.push({ id: item.id, name: item.displayName, driveID: item.id, parentFolder: null});
});
return folders;
});
}
/**
* This function retrieves all Drives for a given Group
*/
public getGroupDrives(group: IFolder): Promise<IFolder[]> {
return this.client
.api(`groups/${group.id}/drives`)
.version('v1.0')
.get()
.then((response): any => {
let folders: Array<IFolder> = new Array<IFolder>();
response.value.forEach((item) => {
folders.push({ id: item.id, name: item.name, driveID: item.id, parentFolder: group});
});
return folders;
});
}
public retrieveMimeMail(driveID: string, folderID: string, mail: IMail, clientCallback: (msg: string)=>void): Promise<string> {
return this.client
.api(`me/messages/${mail.id}/$value`)
.version('v1.0')
.responseType('TEXT')
.get((err: any, response, rawResponse): any => {
if (response.length < (4 * 1024 * 1024)) // If Mail size bigger 4MB use resumable upload
{
this.saveNormalMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback);
}
else {
this.saveBigMail(driveID, folderID, response, Utilities.createMailFileName(mail.subject), clientCallback);
}
});
}
private saveNormalMail(driveID: string, folderID: string, mimeStream: string, fileName: string, clientCallback: (msg: string)=>void) {
const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/content` : `drives/${driveID}/root:/${fileName}.eml:/content`;
this.client
.api(apiUrl)
.put(mimeStream)
.then((response) => {
clientCallback('Success');
})
.catch((error) => {
clientCallback('Error');
});
}
public async saveBigMail(driveID: string, folderID: string, mimeStream: string, fileName: string, clientCallback: (msg: string)=>void) {
const sessionOptions = {
"item": {
"@microsoft.graph.conflictBehavior": "rename"
}
};
const apiUrl = driveID !== folderID ? `drives/${driveID}/items/${folderID}:/${fileName}.eml:/createUploadSession` : `drives/${driveID}/root:/${fileName}.eml:/createUploadSession`;
this.client
.api(apiUrl)
.post(JSON.stringify(sessionOptions))
.then(async (response):Promise<any> => {
console.log(response.uploadUrl);
console.log(response.expirationDateTime);
try {
const resp = await this.uploadMailSlices(mimeStream, response.uploadUrl);
console.log(resp);
clientCallback('Success');
}
catch(err) {
console.log(err);
clientCallback('Error');
}
});
}
private async uploadMailSlices(mimeStream: string, uploadUrl: string) {
let minSize=0;
let maxSize=327680; // 320kb slices
while(mimeStream.length > minSize) {
const fileSlice = mimeStream.slice(minSize, maxSize);
const resp = await this.uploadMailSlice(uploadUrl, minSize, maxSize, mimeStream.length, fileSlice);
minSize = maxSize;
maxSize += 327680;
if (maxSize > mimeStream.length) {
maxSize = mimeStream.length;
}
if (resp.id !== undefined) {
return resp;
}
else {
}
}
}
private async uploadMailSlice(uploadUrl: string, minSize: number, maxSize: number, totalSize: number, fileSlice: string) {
const header = {
"Content-Length": `${maxSize - minSize}`,
"Content-Range": `bytes ${minSize}-${maxSize-1}/${totalSize}`,
};
return await this.client
.api(uploadUrl)
.headers(header)
.put(fileSlice);
}
private saveMailCallback(error: any, response: any, rawResponse?: any): void {
if (error !== null) {
console.log(error);
}
else {
console.log(response);
}
}
}

View File

@ -0,0 +1,8 @@
export default class Utilities {
public static createMailFileName(subject: string): string {
let fileName = subject.replace(/ /g, '_').replace(/:/g, '_');
return fileName;
}
}

View File

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

View File

@ -0,0 +1,7 @@
export interface IFolder {
name: string;
id: string;
driveID: string;
parentFolder: IFolder;
}

View File

@ -0,0 +1,4 @@
export interface IMail {
id: string;
subject: string;
}

View File

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

View File

@ -0,0 +1,69 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'Outlook2SharePointWebPartStrings';
import { IMail } from '../../model/IMail';
import Outlook2SharePoint from './components/Outlook2SharePoint';
import { IOutlook2SharePointProps } from './components/IOutlook2SharePointProps';
export interface IOutlook2SharePointWebPartProps {
description: string;
}
export default class Outlook2SharePointWebPart extends BaseClientSideWebPart <IOutlook2SharePointWebPartProps> {
public render(): void {
let mail: IMail = null;
if (this.context.sdks.office) {
const item = this.context.sdks.office.context.mailbox.item;
if (item !== null) {
mail = { id: item.itemId,subject: item.subject };
}
}
const element: React.ReactElement<IOutlook2SharePointProps> = React.createElement(
Outlook2SharePoint,
{
msGraphClientFactory: this.context.msGraphClientFactory,
mail: mail
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,15 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.folder {
.header {
@include ms-fontSize-m;
margin-left: 3px;
}
.isLink {
cursor: pointer;
}
.sublist {
list-style-type: none;
margin-inline-start: 20px;
}
}

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import styles from './Folder.module.scss';
import { IFolderProps } from './IFolderProps';
export default class Folder extends React.Component<IFolderProps, {}> {
constructor(props) {
super(props);
}
public render(): React.ReactElement<IFolderProps> {
return (
<li className={styles.folder}>
<Icon iconName="DocLibrary" className="ms-IconDocLibrary" />
<span className={`${styles.header} ${styles.isLink}`} onClick={this.getSubFolder}>{this.props.folder.name}</span>
</li>
);
}
private getSubFolder = () => {
this.props.subFolderCallback(this.props.folder);
}
}

View File

@ -0,0 +1,22 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.groups {
margin-bottom: 10px;
.header {
@include ms-fontSize-l;
margin-left: 6px;
}
.list {
list-style-type: none;
padding-inline-start: 20px;
}
.saveBtn {
margin: 5px 10px 5px 10px;
}
.spinnerContainer {
position: relative;
@include ms-md12;
@include ms-lg10;
@include ms-xl8;
}
}

View File

@ -0,0 +1,161 @@
import * as React from 'react';
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import Breadcrumb from './controls/Breadcrumb';
import Folder from './Folder';
import styles from './Groups.module.scss';
import { IGroupsProps } from './IGroupsProps';
import { IGroupsState } from './IGroupsState';
import { IFolder } from '../../../model/IFolder';
export default class Groups extends React.Component<IGroupsProps, IGroupsState> {
constructor(props) {
super(props);
this.state = {
folders: [],
grandParentFolder: null,
parentFolder: null,
showSpinner: false
};
}
public componentDidMount() {
if (this.props.graphController !== null) {
this.getGroups();
}
}
public render(): React.ReactElement<IGroupsProps> {
let fldrs = this.state.folders.map((fldr) => {
return <Folder folder={fldr} subFolderCallback={fldr.parentFolder===null?this.getGroupDrives:this.getSubFolders}></Folder>;
});
return (
<div className={styles.groups}>
<div>
<div>
<Breadcrumb
grandParentFolder={this.state.grandParentFolder}
parentFolder={this.state.parentFolder}
rootCallback={this.showRoot}
parentFolderCallback={this.showParentFolder}>
</Breadcrumb>
</div>
</div>
<div className="recent-content">
<ul className={styles.list}>
{fldrs}
</ul>
</div>
<div>
<PrimaryButton
className={styles.saveBtn}
text="Save here"
onClick={this.saveMailTo}
disabled={this.state.parentFolder === null}
allowDisabledFocus={true}
/>
{ this.state.showSpinner && (
<div className={styles.spinnerContainer}>
<Overlay >
<Spinner size={ SpinnerSize.large } label='Processing request' />
</Overlay>
</div>
) }
</div>
</div>
);
}
private getGroups = () => {
this.props.graphController.getJoinedGroups().then((folders) => {
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
return {
folders: folders
};
});
});
}
private getGroupDrives = (group: IFolder) => {
let nextParent: IFolder = null;
this.state.folders.forEach((fldr) => {
if (fldr.id === group.id) {
nextParent = fldr;
}
});
this.props.graphController.getGroupDrives(group).then((folders) => {
if (folders.length > 0) {
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
return {
folders: folders,
grandParentFolder: null,
parentFolder: group
};
});
}
});
}
private getSubFolders = (folder: IFolder) => {
if (folder.id === folder.driveID) {
this.props.graphController.getGroupRootFolders(folder).then((folders) => {
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
return {
folders: folders,
grandParentFolder: folder.parentFolder,
parentFolder: folder
};
});
});
}
else {
this.props.graphController.getSubFolder(folder).then((folders) => {
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
return {
folders: folders,
grandParentFolder: folder.parentFolder,
parentFolder: folder
};
});
});
}
}
private showRoot = () => {
this.getGroups();
}
private showParentFolder = (parentFolder: IFolder) => {
if (this.state.grandParentFolder===null) {
this.getGroupDrives(parentFolder);
}
else {
this.getSubFolders(parentFolder);
}
}
private saveMailTo = () => {
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
return {
showSpinner: true
};
});
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback);
}
private saveMailCallback = (message: string) => {
this.setState((prevState: IGroupsState, props: IGroupsProps) => {
return {
showSpinner: false
};
});
if (message.indexOf('Success') > -1) {
this.props.successCallback(message);
}
else {
this.props.errorCallback(message);
}
}
}

View File

@ -0,0 +1,6 @@
import { IFolder } from '../../../model/IFolder';
export interface IFolderProps {
folder: IFolder;
subFolderCallback: (folder: IFolder) => void;
}

View File

@ -0,0 +1,10 @@
import GraphController from '../../../controller/GraphController';
import { IMail } from '../../../model/IMail';
export interface IGroupsProps {
graphController: GraphController;
mail: IMail;
successCallback: (msg: string) => void;
errorCallback: (msg: string) => void;
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../model/IFolder';
export interface IGroupsState {
folders: IFolder[];
grandParentFolder: IFolder;
parentFolder: IFolder;
showSpinner: boolean;
}

View File

@ -0,0 +1,9 @@
import GraphController from '../../../controller/GraphController';
import { IMail } from '../../../model/IMail';
export interface IOneDriveProps {
graphController: GraphController;
mail: IMail;
successCallback: (msg: string) => void;
errorCallback: (msg: string) => void;
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../model/IFolder';
export interface IOneDriveState {
folders: IFolder[];
grandParentFolder: IFolder;
parentFolder: IFolder;
showSpinner: boolean;
}

View File

@ -0,0 +1,7 @@
import { MSGraphClientFactory } from '@microsoft/sp-http';
import { IMail } from '../../../model/IMail';
export interface IOutlook2SharePointProps {
mail: IMail;
msGraphClientFactory: MSGraphClientFactory;
}

View File

@ -0,0 +1,12 @@
import GraphController from '../../../controller/GraphController';
export interface IOutlook2SharePointState {
graphController: GraphController;
showSuccess: boolean;
showError: boolean;
showOneDrive: boolean;
showTeams: boolean;
showGroups: boolean;
successMessage: string;
errorMessage: string;
}

View File

@ -0,0 +1,10 @@
import GraphController from '../../../controller/GraphController';
import { IMail } from '../../../model/IMail';
export interface ITeamsProps {
graphController: GraphController;
mail: IMail;
successCallback: (msg: string) => void;
errorCallback: (msg: string) => void;
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../model/IFolder';
export interface ITeamsState {
folders: IFolder[];
grandParentFolder: IFolder;
parentFolder: IFolder;
showSpinner: boolean;
}

View File

@ -0,0 +1,122 @@
import * as React from 'react';
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import Folder from './Folder';
import styles from './Groups.module.scss';
import Breadcrumb from './controls/Breadcrumb';
import { IOneDriveProps } from './IOneDriveProps';
import { IOneDriveState } from './IOneDriveState';
import { IFolder } from '../../../model/IFolder';
export default class OneDrive extends React.Component<IOneDriveProps, IOneDriveState> {
constructor(props) {
super(props);
this.state = {
folders: [],
grandParentFolder: null,
parentFolder: null,
showSpinner: false
};
}
public componentDidMount() {
if (this.props.graphController !== null) {
this.getFolder();
}
}
public render(): React.ReactElement<IOneDriveProps> {
let fldrs = this.state.folders.map((fldr) => {
return <Folder folder={fldr} subFolderCallback={this.getSubFolders}></Folder>;
});
return (
<div className={styles.groups}>
<div>
<div>
<Breadcrumb
grandParentFolder={this.state.grandParentFolder}
parentFolder={this.state.parentFolder}
rootCallback={this.showRoot}
parentFolderCallback={this.showParentFolder}>
</Breadcrumb>
</div>
</div>
<div className="recent-content">
<ul className={styles.list}>
{fldrs}
</ul>
</div>
<div>
<PrimaryButton
className={styles.saveBtn}
text="Save here"
onClick={this.saveMailTo}
allowDisabledFocus={true}
/>
{ this.state.showSpinner && (
<div className={styles.spinnerContainer}>
<Overlay >
<Spinner size={ SpinnerSize.large } label='Processing request' />
</Overlay>
</div>
) }
</div>
</div>
);
}
private getFolder = () => {
this.props.graphController.getOneDriveFolder().then((folders) => {
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
return {
folders: folders
};
});
});
}
private getSubFolders = (folder: IFolder) => {
this.props.graphController.getSubFolder(folder).then((folders) => {
if (folders.length > 0) {
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
return {
folders: folders,
grandParentFolder: folder.parentFolder,
parentFolder: folder
};
});
}
});
}
private showRoot = () => {
this.getFolder();
}
private showParentFolder = (parentFolder: IFolder) => {
this.getSubFolders(parentFolder);
}
private saveMailTo = () => {
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
return {
showSpinner: true
};
});
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback);
}
private saveMailCallback = (message: string) => {
this.setState((prevState: IOneDriveState, props: IOneDriveProps) => {
return {
showSpinner: false
};
});
if (message.indexOf('Success') > -1) {
this.props.successCallback(message);
}
else {
this.props.errorCallback(message);
}
}
}

View File

@ -0,0 +1,17 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.outlook2SharePoint {
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
.header {
cursor: pointer;
margin-left: 6px;
}
.headerIcon {
font-size: 1.5em;
font-weight: bolder;
}
.headerText {
@include ms-fontSize-l;
margin-left: 3px;
}
}

View File

@ -0,0 +1,175 @@
import * as React from 'react';
import styles from './Outlook2SharePoint.module.scss';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import GraphController from '../../../controller/GraphController';
import Groups from './Groups';
import OneDrive from './OneDrive';
import Teams from './Teams';
import { IOutlook2SharePointProps } from './IOutlook2SharePointProps';
import { IOutlook2SharePointState } from './IOutlook2SharePointState';
export default class Outlook2SharePoint extends React.Component<IOutlook2SharePointProps, IOutlook2SharePointState> {
private graphController: GraphController;
constructor(props) {
super(props);
this.state = {
graphController: null,
showError: false,
showSuccess: false,
showOneDrive: false,
showTeams: false,
showGroups: false,
successMessage: '',
errorMessage: ''
};
this.graphController = new GraphController(this.props.msGraphClientFactory, this.graphClientReadyCallback);
}
public render(): React.ReactElement<IOutlook2SharePointProps> {
return (
<div className={ styles.outlook2SharePoint }>
{this.state.showSuccess && <div>
<MessageBar
messageBarType={MessageBarType.success}
isMultiline={false}
onDismiss={this.closeMessage}
dismissButtonAriaLabel="Close"
truncated={true}
overflowButtonAriaLabel="See more"
>
{this.state.successMessage}
</MessageBar>
</div>}
{this.state.showError && <div>
<MessageBar
messageBarType={MessageBarType.error}
isMultiline={false}
onDismiss={this.closeMessage}
dismissButtonAriaLabel="Close"
truncated={true}
overflowButtonAriaLabel="See more"
>
{this.state.errorMessage}
</MessageBar>
</div>}
<div className={styles.header} onClick={this.showOneDrive}>
<Icon iconName="OneDrive" className={`ms-IconOneDrive ${styles.headerIcon}`} />
<span className={styles.headerText}>OneDrive</span>
</div>
{this.state.graphController && this.state.showOneDrive &&
<OneDrive
graphController={this.state.graphController}
mail={this.props.mail}
successCallback={this.showSuccess}
errorCallback={this.showError}>
</OneDrive>}
<div className={styles.header} onClick={this.showTeams}>
<Icon iconName="TeamsLogo" className={`ms-IconTeamsLogo ${styles.headerIcon}`} />
<span className={styles.headerText}>Microsoft Teams</span>
</div>
{this.state.graphController && this.state.showTeams &&
<Teams
graphController={this.state.graphController}
mail={this.props.mail}
successCallback={this.showSuccess}
errorCallback={this.showError}>
</Teams>}
<div className={styles.header} onClick={this.showGroups}>
<Icon iconName="Group" className={`ms-IconGroup ${styles.headerIcon}`} />
<span className={styles.headerText}>Microsoft Groups</span>
</div>
{this.state.graphController && this.state.showGroups &&
<Groups
graphController={this.state.graphController}
mail={this.props.mail}
successCallback={this.showSuccess}
errorCallback={this.showError}>
</Groups>}
</div>
);
}
/**
* This function first retrieves all OneDrive root folders from user
*/
private graphClientReadyCallback = () => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
graphController: this.graphController
};
});
}
private showError = (message: string) => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
showError: true,
showSuccess: false,
errorMessage: message
};
});
}
private showSuccess = (message: string) => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
showSuccess: true,
showError: false,
successMessage: message
};
});
}
private closeMessage = () => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
showSuccess: false,
showError: false
};
});
}
/**
* This function expands the Teams section and collapses the other ones
*/
private showTeams = () => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
showTeams: true,
showOneDrive: false,
showGroups: false
};
});
}
/**
* This function expands the OneDrive section and collapses the other ones
*/
private showOneDrive = () => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
showOneDrive: true,
showTeams: false,
showGroups: false
};
});
}
/**
* This function expands the Groups section and collapses the other ones
*/
private showGroups = () => {
this.setState((prevState: IOutlook2SharePointState, props: IOutlook2SharePointProps) => {
return {
showGroups: true,
showTeams: false,
showOneDrive: false
};
});
}
}

View File

@ -0,0 +1,161 @@
import * as React from 'react';
import { Overlay } from 'office-ui-fabric-react/lib/Overlay';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import Breadcrumb from './controls/Breadcrumb';
import Folder from './Folder';
import styles from './Groups.module.scss';
import { ITeamsProps } from './ITeamsProps';
import { ITeamsState } from './ITeamsState';
import { IFolder } from '../../../model/IFolder';
export default class Teams extends React.Component<ITeamsProps, ITeamsState> {
constructor(props) {
super(props);
this.state = {
folders: [],
grandParentFolder: null,
parentFolder: null,
showSpinner: false
};
}
public componentDidMount() {
if (this.props.graphController !== null) {
this.getTeams();
}
}
public render(): React.ReactElement<ITeamsState> {
let fldrs = this.state.folders.map((fldr) => {
return <Folder folder={fldr} subFolderCallback={fldr.parentFolder===null?this.getGroupDrives:this.getSubFolders}></Folder>;
});
return (
<div className={styles.groups}>
<div>
<div>
<Breadcrumb
grandParentFolder={this.state.grandParentFolder}
parentFolder={this.state.parentFolder}
rootCallback={this.showRoot}
parentFolderCallback={this.showParentFolder}>
</Breadcrumb>
</div>
</div>
<div className="recent-content">
<ul className={styles.list}>
{fldrs}
</ul>
</div>
<div>
<PrimaryButton
className={styles.saveBtn}
text="Save here"
onClick={this.saveMailTo}
disabled={this.state.parentFolder === null}
allowDisabledFocus={true}
/>
{ this.state.showSpinner && (
<div className={styles.spinnerContainer}>
<Overlay >
<Spinner size={ SpinnerSize.large } label='Processing request' />
</Overlay>
</div>
) }
</div>
</div>
);
}
private getTeams = () => {
this.props.graphController.getJoinedTeams().then((folders) => {
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
return {
folders: folders
};
});
});
}
private getGroupDrives = (group: IFolder) => {
let nextParent: IFolder = null;
this.state.folders.forEach((fldr) => {
if (fldr.id === group.id) {
nextParent = fldr;
}
});
this.props.graphController.getGroupDrives(group).then((folders) => {
if (folders.length > 0) {
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
return {
folders: folders,
grandParentFolder: null,
parentFolder: group
};
});
}
});
}
private getSubFolders = (folder: IFolder) => {
if (folder.id === folder.driveID) {
this.props.graphController.getGroupRootFolders(folder).then((folders) => {
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
return {
folders: folders,
grandParentFolder: folder.parentFolder,
parentFolder: folder
};
});
});
}
else {
this.props.graphController.getSubFolder(folder).then((folders) => {
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
return {
folders: folders,
grandParentFolder: folder.parentFolder,
parentFolder: folder
};
});
});
}
}
private showRoot = () => {
this.getTeams();
}
private showParentFolder = (parentFolder: IFolder) => {
if (this.state.grandParentFolder===null) {
this.getGroupDrives(parentFolder);
}
else {
this.getSubFolders(parentFolder);
}
}
private saveMailTo = () => {
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
return {
showSpinner: true
};
});
this.props.graphController.retrieveMimeMail(this.state.parentFolder.driveID, this.state.parentFolder.id, this.props.mail, this.saveMailCallback);
}
private saveMailCallback = (message: string) => {
this.setState((prevState: ITeamsState, props: ITeamsProps) => {
return {
showSpinner: false
};
});
if (message.indexOf('Success') > -1) {
this.props.successCallback(message);
}
else {
this.props.errorCallback(message);
}
}
}

View File

@ -0,0 +1,28 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.breadcrumb {
@include ms-Grid;
.rootIcon {
cursor: pointer;
margin-right: 5px;
@include ms-Grid-col;
padding-left: 0px;
padding-right: 0px;
}
.row {
@include ms-Grid-row;
}
.grandParent {
@include ms-Grid-col;
padding-left: 0px;
padding-right: 0px;
margin-right: 5px;
}
.link {
cursor: pointer;
}
.nonLink {
margin-left: 5px;
font-weight: bolder;
}
}

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import styles from './Breadcrumb.module.scss';
import { IBreadcrumbProps } from './IBreadcrumbProps';
//import { IFolderState } from './IFolderState';
export default class Breadcrumb extends React.Component<IBreadcrumbProps, {}> {
constructor(props) {
super(props);
this.state = {
subFolders: []
};
}
public render(): React.ReactElement<IBreadcrumbProps> {
return (
<div className={styles.breadcrumb}>
{this.props.grandParentFolder !== null && this.props.parentFolder !== null &&
<Icon onClick={this.showRoot} iconName="DoubleChevronLeft" className={`ms-IconDoubleChevronLeft ${styles.rootIcon}`} />}
<div className={styles.row}>
{this.props.grandParentFolder &&
<div className={styles.grandParent}>
<span className={styles.link} onClick={this.showParentFolder}>{this.props.grandParentFolder.name}</span>
</div>}
{this.props.parentFolder &&
<div className={styles.grandParent}>
<Icon iconName="ChevronRight" className="ms-IconChevronRight" />
<span className={styles.nonLink}>{this.props.parentFolder.name}</span>
</div>}
</div>
</div>
);
}
private showRoot = () => {
this.props.rootCallback();
}
private showParentFolder = () => {
this.props.parentFolderCallback(this.props.grandParentFolder);
}
}

View File

@ -0,0 +1,8 @@
import { IFolder } from '../../../../model/IFolder';
export interface IBreadcrumbProps {
grandParentFolder: IFolder;
parentFolder: IFolder;
rootCallback: () => void;
parentFolderCallback: (folder: IFolder) => void;
}

View File

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

View File

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

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