react-feedback commit.submit feedback on sitepages

This commit is contained in:
Perry Kankam 2020-12-15 18:41:38 -05:00
commit f9606eeed8
38 changed files with 19627 additions and 0 deletions

25
.editorconfig Normal file
View File

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

32
.gitignore vendored Normal file
View File

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

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"msjsdiag.debugger-for-chrome"
]
}

43
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,43 @@
{
/**
* Install Chrome Debugger Extension for Visual Studio Code to debug your components with the
* Chrome browser: https://aka.ms/spfx-debugger-extensions
*/
"version": "0.2.0",
"configurations": [{
"name": "Local workbench",
"type": "chrome",
"request": "launch",
"url": "https://localhost:4321/temp/workbench.html",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222"
]
},
{
"name": "Hosted workbench",
"type": "chrome",
"request": "launch",
"url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222",
"-incognito"
]
}
]
}

13
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
// Configure glob patterns for excluding files and folders in the file explorer.
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bower_components": true,
"**/coverage": true,
"**/lib-amd": true,
"src/**/*.scss.ts": true
},
"typescript.tsdk": ".\\node_modules\\typescript\\lib"
}

12
.yo-rc.json Normal file
View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.11.0",
"libraryName": "feedback-webpart",
"libraryId": "d5e255e1-7071-49a8-b50d-b06c80e4ac02",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

66
README.md Normal file
View File

@ -0,0 +1,66 @@
# Feedback
## Summary
This is an application that supports Feedback through a web part that can be used directly on a Modern Sharepoint Site page. This webpart can be added to any site page or article. This allows users to send categorized feedback via email to users in the "Feedback Owners" group.
![Feedback](./assets/feedbackwebpart.gif)
## Used SharePoint Framework Version
![1.11.0](https://img.shields.io/badge/version-1.11.0-green.svg)
## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## Prerequisites
* Office 365 subscription with SharePoint Online
* SharePoint Framework [development environment](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment) set up
## Solution
Solution|Author(s)
--------|---------
react-feedback | Perry Kankam
## Version history
Version|Date|Comments
-------|----|--------
1.0|December 15, 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
*To really get the full experience go to the workbench on a SharePoint Site [Your site url]/_layouts/15/workbench.aspx and that's where the magic will happen but this requires that you deploy and activate features to provision the required SharePoint assets*
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
* Add "Feedback Owners" Sharepoint group. This is where you'll add all users who should receive this feedback.
* Run one of the following custom commands to clean, build, bundle and package the solution.
* If you want to be able to debug using your local code using gulp serve
`gulp package`
* Navigate to the output `feedback-webpart.sppkg` (found in the `/sharepoint/solution` folder)
* Upload it to an application catalog (either a tenant or site collection one)
* In your site collection go to **Site Contents** and click **New** > **App**
* Find and add the **Feedback Application** App
* wait for it to finish installing and activating features on the **Site Contents** page
* Go to a site page like home, edit the page and find and add the **Feedback** web part
* If you deployed a shippable (SharePoint Online) version you don't need to do anything else
* If you deployed a debug (http://localhost:4321) version you'll need to ensure gulp serve is running
## Features
This sample illustrates the following concepts:
- Used [@pnp/polyfill-ie11](https://pnp.github.io/pnpjs/concepts/polyfill/)
- Used [PnP](https://pnp.github.io/pnpjs/) for communication with SharePoint.
- Used [@pnp/logging](https://pnp.github.io/pnpjs/logging/)

BIN
assets/feedbackwebpart.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

18
config/config.json Normal file
View File

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

4
config/copy-assets.json Normal file
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": "feedback-webpart",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "feedback-webpart-client-side-solution",
"id": "d5e255e1-7071-49a8-b50d-b06c80e4ac02",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
}
},
"paths": {
"zippedPackage": "solution/feedback-webpart.sppkg"
}
}

10
config/serve.json Normal file
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 -->"
}

12
gulpfile.js Normal file
View File

@ -0,0 +1,12 @@
'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);
var runSequence = require('run-sequence');
gulp.task('package', function (cb) {
runSequence('clean', 'build', 'bundle', 'package-solution', cb);
});

18544
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "feedback-webpart",
"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.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"@pnp/polyfill-ie11": "^2.0.2",
"@pnp/sp": "^2.0.13",
"run-sequence": "^2.2.1"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.11.0",
"@microsoft/sp-tslint-rules": "1.11.0",
"@microsoft/sp-module-interfaces": "1.11.0",
"@microsoft/sp-webpart-workbench": "1.11.0",
"@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",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33"
}
}

1
src/index.ts Normal file
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,4 @@
export interface IArticleInfo {
title: string;
url: string;
}

View File

@ -0,0 +1,46 @@
import { HttpRequestError } from '@pnp/odata';
import { LogHelper } from "../utilities";
export class BaseService {
constructor() {
}
public handleHttpError(methodName: string, error: HttpRequestError): void {
this.logError(methodName, error);
}
public logError(methodName: string, error: Error) {
LogHelper.exception(this.constructor.name, methodName, error);
}
public logPnpError(methodName: string, error: HttpRequestError | any): string {
let msg: string;
if (error instanceof HttpRequestError) {
if (error.message) {
msg = error.message;
LogHelper.error(this.constructor.name, methodName, msg);
}
else {
LogHelper.exception(this.constructor.name, methodName, error);
}
}
else if (error.data != null && error.data.responseBody && error.data.responseBody.error && error.data.responseBody.error.message) {
// for email exceptions they weren't coming in as "instanceof HttpRequestError"
msg = error.data.responseBody.error.message.value;
LogHelper.error(this.constructor.name, methodName, msg);
}
else if (error instanceof Error) {
if (error.message.indexOf('[412] Precondition Failed') !== -1) {
msg = 'Save Conflict. Your changes conflict with those made concurrently by another user. If you want your changes to be applied, resubmit your changes.';
LogHelper.error(this.constructor.name, methodName, msg);
}
else if (error.message !== 'Unexpected token < in JSON at position 0') {
// 'Unexpected token < in JSON at position 0' will be thrown if XML file is read; this was issue in MDF project
msg = error.message;
LogHelper.error(this.constructor.name, methodName, msg);
}
return msg;
}
}
}

View File

@ -0,0 +1,99 @@
import { sp } from '@pnp/sp/presets/all';
import { IPrincipalInfo } from "@pnp/sp";
import "@pnp/sp/sputilities";
import "@pnp/polyfill-ie11";
import { IEmailProperties } from "@pnp/sp/sputilities";
import "@pnp/sp/webs";
import "@pnp/sp/site-users/web";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/site-groups/web";
// import { EmailProperties } from '@pnp/sp';
import { BaseService } from './base.service';
import { LogHelper } from '../utilities';
import { IArticleInfo } from '../models/IArticleInfo';
export class FeedbackService extends BaseService {
public async getTitle(listitemid): Promise<any> {
var item = await sp.web.lists.getByTitle("Site Pages").items.getById(listitemid).select("Title").get();
var itemTitle = item["Title"];
return itemTitle;
}
public async getArticleInfo(listitemid): Promise<IArticleInfo> {
var item = await sp.web.lists.getByTitle("Site Pages").items.getById(listitemid).select("Title", "EncodedAbsUrl").get();
let articleInfo: IArticleInfo = {
title: item["Title"],
url: item["EncodedAbsUrl"]
};
return articleInfo;
}
public async sendEmailToFeedbackSender(articleInfo: IArticleInfo, feedback, listitemid, currentUserEmail): Promise<any> {
if (feedback.indexOf("\n") > -1) {
feedback = feedback.replace(/\n/g, '<br/>');
}
if (currentUserEmail) {
console.log("Email sending to User: " + currentUserEmail);
const emailProps: IEmailProperties = {
To: [currentUserEmail],
Subject: (articleInfo.title == "Home" || listitemid == 1) ? "Feedback has been provided on the Homepage" : "Feedback for " + articleInfo.title,
Body: (articleInfo.title == "Home" || listitemid == 1) ? "The feedback you provided on the Homepage has successfully been submitted.</br></br>\"" + feedback + "\"<br/>"
: "The feedback you provided on \"" + articleInfo.title + "\" has been successfully submitted.</br></br>\"" + feedback + "\"<br/><br>Article can be found here: <a href=\"" + articleInfo.url + "\">" + articleInfo.url + "</a></br>"
};
await sp.utility.sendEmail(emailProps)
.catch(e => {
super.handleHttpError('sendEmail', e);
throw e;
});
}
return;
}
public async sendEmailToOwnerGroup(feedback, listitemid, category, currentUserName, currentUserEmail) {
let articleInfo = await this.getArticleInfo(listitemid);
let principals: IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals(["Feedback Owners"]);
if (feedback.indexOf("\n") > -1) {
feedback = feedback.replace(/\n/g, '<br/>');
}
var emails: string[] = [];
for (var i = 0; i < principals.length; i++) {
if (principals[i].Email) {
emails.push(principals[i].Email);
}
else {
LogHelper.warning("FeedbackService", "sendEmailToOwnerGroup", `No email for ${principals[i].LoginName}`);
}
}
console.log("Owner Emails: " + emails.join(";"));
if (emails && emails.length > 0) {
const emailProps: IEmailProperties = {
To: emails,
Subject: (articleInfo.title == "Home") ? "Feedback has been provided on the Homepage (" + category + ")" : "Feedback for " + articleInfo.title + " (" + category + ")",
Body: (articleInfo.title == "Home" || listitemid == 1) ? "\"" + feedback + "\"<br/><br/>Submitted by: " + currentUserName + " <a href=\"mailto:" + currentUserEmail + "\">" + currentUserEmail + "</a><br/>"
: "\"" + feedback + "\"<br/><br/>Submitted by: " + currentUserName + " <a href=\"mailto:" + currentUserEmail + "\">" + currentUserEmail + "</a><<br/><br/>Article can be found here: <a href=\"" + articleInfo.url + "\">" + articleInfo.url + "</a>"
};
await sp.utility.sendEmail(emailProps)
.catch(e => {
super.handleHttpError('sendEmail', e);
throw e;
});
LogHelper.info("FeedbackService", "sendEmailToOwnerGroup", `Email Sent`);
}
this.sendEmailToFeedbackSender(articleInfo, feedback, listitemid, currentUserEmail);
}
}

View File

@ -0,0 +1,36 @@
import { Logger, LogLevel } from "@pnp/logging";
export class LogHelper {
public static verbose(className: string, methodName: string, message: string) {
message = this.formatMessage(className, methodName, message);
Logger.write(message, LogLevel.Verbose);
}
public static info(className: string, methodName: string, message: string) {
message = this.formatMessage(className, methodName, message);
Logger.write(message, LogLevel.Info);
}
public static warning(className: string, methodName: string, message: string) {
message = this.formatMessage(className, methodName, message);
Logger.write(message, LogLevel.Warning);
}
public static error(className: string, methodName: string, message: string) {
message = this.formatMessage(className, methodName, message);
Logger.write(message, LogLevel.Error);
}
public static exception(className: string, methodName: string, error: Error) {
error.message = this.formatMessage(className, methodName, error.message);
Logger.error(error);
}
private static formatMessage(className: string, methodName: string, message: string): string {
let d = new Date();
let dateStr = d.getDate() + '-' + (d.getMonth() + 1) + '-' + d.getFullYear() + ' ' +
d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds() + '.' + d.getMilliseconds();
return `${dateStr} ${className} > ${methodName} > ${message}`;
}
}

View File

@ -0,0 +1,51 @@
$ms-greenLight : "[theme:greenLight, default:#bad80a]";
$ms-neutralSecondaryAlt : "[theme:info, default:#767676]";
$ms-neutralLight : "[theme:infoBackground, default:#eaeaea]";
$ms-magenta : "[theme:magenta, default:#b4009e]";
$ms-magentaDark : "[theme:magentaDark, default:#5c005c]";
$ms-magentaLight : "[theme:magentaLight, default:#e3008c]";
$ms-neutralDark : "[theme:neutralDark, default:#212121]";
$ms-neutralLight : "[theme:neutralLight, default:#eaeaea]";
$ms-neutralLighter : "[theme:neutralLighter, default:#f4f4f4]";
$ms-neutralLighterAlt : "[theme:neutralLighterAlt, default:#f8f8f8]";
$ms-neutralPrimary : "[theme:neutralPrimary, default:#333333]";
$ms-neutralPrimaryAlt : "[theme:neutralPrimaryAlt, default:#3C3C3C]";
$ms-neutralQuaternary : "[theme:neutralPrimaryTranslucent50, default:#d0d0d0]";
$ms-neutralQuaternaryAlt : "[theme:neutralQuaternary, default:#dadada]";
$ms-neutralSecondary : "[theme:neutralQuaternaryAlt, default:#666666]";
$ms-neutralSecondaryAlt : "[theme:neutralSecondary, default:#767676]";
$ms-neutralTertiary : "[theme:neutralSecondaryAlt, default:#a6a6a6]";
$ms-neutralTertiaryAlt : "[theme:neutralTertiary, default:#c8c8c8]";
$ms-white : "[theme:neutralTertiaryAlt, default:#ffffff]";
$ms-orange : "[theme:orange, default:#d83b01]";
$ms-orangeLight : "[theme:orangeLight, default:#ea4300]";
$ms-orangeLighter : "[theme:orangeLighter, default:#ff8c00]";
$ms-primaryBackground : "[theme:primaryBackground, default:#0078d7]";
$ms-primaryText : "[theme:primaryText, default:#0078d7]";
$ms-purple : "[theme:purple, default:#5c2d91]";
$ms-purpleDark : "[theme:purpleDark, default:#32145a]";
$ms-purpleLight : "[theme:purpleLight, default:#b4a0ff]";
$ms-red : "[theme:red, default:#e81123]";
$ms-redDark : "[theme:redDark, default:#a80000]";
$ms-success : "[theme:success, default:#107c10]";
$ms-successBackground : "[theme:successBackground, default:#dff6dd]";
$ms-teal : "[theme:teal, default:#008272]";
$ms-tealDark : "[theme:tealDark, default:#004b50]";
$ms-tealLight : "[theme:tealLight, default:#00b294]";
$ms-themeAccent : "[theme:themeAccent, default:inherit]";
$ms-themeAccentTranslucent10 : "[theme:themeAccentTranslucent10, default:inherit]";
$ms-themeDark : "[theme:themeDark, default:#005a9e]";
$ms-themeDarkAlt : "[theme:themeDarkAlt, default:#106ebe]";
$ms-themeDarker : "[theme:themeDarker, default:#004578]";
$ms-themeLight : "[theme:themeLight, default:#b3d6f2]";
$ms-themeLightAlt : "[theme:themeLightAlt, default:inherit]";
$ms-themeLighter : "[theme:themeLighter, default:#deecf9]";
$ms-themeLighterAlt : "[theme:themeLighterAlt, default:#eff6fc]";
$ms-themePrimary : "[theme:themePrimary, default:#0078d7]";
$ms-themeSecondary : "[theme:themeSecondary, default:#2488d8]";
$ms-themeTertiary : "[theme:themeTertiary, default:#69afe5]";
$ms-themeTertiaryAlt : "[theme:themeTertiaryAlt, default:#c8c8c8]";
$ms-white : "[theme:white, default:#ffffff]";
$ms-whiteTranslucent40 : "[theme:whiteTranslucent40, default:rgba(255,255,255,.4)]";
$ms-yellow : "[theme:yellow, default:#ffb900]";
$ms-yellowLight : "[theme:yellowLight, default:#fff100]";

1
src/utilities/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './LogHelper';

View File

@ -0,0 +1,25 @@
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { sp } from "@pnp/sp";
import { Logger, ConsoleListener, LogLevel } from "@pnp/logging";
export default class BaseWebPart<TProperties> extends BaseClientSideWebPart<TProperties> {
protected async onInit(): Promise<void> {
return super.onInit().then(_ => {
sp.setup({
ie11: true,
spfxContext: this.context,
});
// subscribe a listener
Logger.subscribe(new ConsoleListener());
// set the active log level -- eventually make this a web part property
Logger.activeLogLevel = LogLevel.Error;
});
}
public render(): void {
}
}

View File

@ -0,0 +1,16 @@
export class DropdownOptions {
public static Options = [
{
key: "general",
text: "General"
},
{
key: "typo",
text: "Typo/Edit/Broken Link"
},
{
key: "suggestion",
text: "Suggestion"
}
];
}

View File

@ -0,0 +1,32 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "58f4aa4b-7256-42d7-880c-ba4f51d3f6ab",
"alias": "FeedbackWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportedHosts": [
"SharePointWebPart"
],
"preconfiguredEntries": [
{
"groupId": "75e22ed5-fa14-4829-850a-c890608aca2d",
"group": {
"default": "Communication and collaboration"
},
"title": {
"default": "Feedback"
},
"description": {
"default": "Feedback webpart to submit feedback on pages/articles."
},
"officeFabricIconFontName": "Feedback",
"properties": {
"buttonLabel": "Submit Feedback",
"feedbackCategory": "general",
"showCategory": true
}
}
]
}

View File

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

View File

@ -0,0 +1,111 @@
import { Version } from '@microsoft/sp-core-library';
import "@pnp/polyfill-ie11";
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { ThemeProvider, IReadonlyTheme, ThemeChangedEventArgs } from '@microsoft/sp-component-base';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
PropertyPaneLabel,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import styles from './FeedbackWebPart.module.scss';
import * as strings from 'FeedbackWebPartStrings';
import BaseWebPart from '../BaseWebPart';
import {Container} from './components/Container/Container';
import {IContainerProps} from './components/Container/IContainerProps';
import {DropdownOptions} from './DropdownOptions';
export interface IFeedbackWebPartProps {
buttonLabel: string;
feedbackCategory: string;
showCategory: boolean;
}
export default class FeedbackWebPart extends BaseWebPart<IFeedbackWebPartProps> {
private themeProvider: ThemeProvider;
private themeVariant: IReadonlyTheme | undefined;
protected onInit(): Promise<void> {
// Consume the new ThemeProvider service
this.themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
// If it exists, get the theme variant
this.themeVariant = this.themeProvider.tryGetTheme();
// Register a handler to be notified if the theme variant changes
this.themeProvider.themeChangedEvent.add(this, this.handleThemeChangedEvent);
return super.onInit();
}
public async render(): Promise<void> {
var showCategory = escape(this.properties.showCategory.toString()) == "false" ? false : true;
const element: React.ReactElement<IContainerProps> = React.createElement(
Container,
{
buttonLabel: escape(this.properties.buttonLabel),
showCategory: showCategory,
themeVariant: this.themeVariant,
listitemid: this.context.pageContext.listItem.id, //Replace with "1" if you're running this in a workbench
selectedCategory: escape(this.properties.feedbackCategory),
currentUser: this.context.pageContext.user
}
);
ReactDom.render(element, this.domElement);
}
private handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
this.themeVariant = args.theme;
this.render();
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPane_Description
},
groups: [
{
groupName: strings.PropertyPane_GroupName_Settings,
groupFields: [
PropertyPaneTextField('buttonLabel', {
label: strings.PropertyPane_Label_ButtonText
}),
PropertyPaneToggle('showCategory', {
label: strings.FeedbackCategoryToggle_Label,
onText: 'On',
offText: 'Off'
}),
PropertyPaneDropdown('feedbackCategory', {
label: strings.FeedbackCategory_Label,
selectedKey: "general",
options: DropdownOptions.Options})
]
},
{
groupName: strings.PropertyPane_GroupName_About,
groupFields: [
PropertyPaneLabel('versionNumber', {
text: strings.PropertyPane_Label_VersionInfo + this.manifest.version
})
]}
]
}
]
};
}
}

View File

@ -0,0 +1,161 @@
import * as React from 'react';
import "@pnp/polyfill-ie11";
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react/lib/Dialog';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { IContainerProps } from './IContainerProps';
import { useConstCallback } from '@uifabric/react-hooks';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Dropdown, DropdownMenuItemType, IDropdownStyles, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { FeedbackService } from '../../../../services/feedback.service';
import { DropdownOptions } from '../../DropdownOptions';
import * as strings from 'FeedbackWebPartStrings';
import { IArticleInfo } from '../../../../models/IArticleInfo';
const buttonStyles = { root: { marginRight: 8 } };
const dialogContentProps = {
type: DialogType.normal,
title: 'Feedback successfully submitted.',
};
const dialogModalProps = {
isBlocking: false,
styles: { main: { maxWidth: 450 } },
};
const options: IDropdownOption[] = DropdownOptions.Options;
export const Container: React.FunctionComponent<IContainerProps> = props => {
//This is for IE 11 "find" issue
// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
value: function(predicate) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
// tslint:disable-next-line: use-named-parameter
var thisArg = arguments[1];
// 5. Let k be 0.
var k = 0;
// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return kValue.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return kValue;
}
// e. Increase k by 1.
k++;
}
// 7. Return undefined.
return undefined;
}
});
}
let defaultCategoryValue = DropdownOptions.Options.find(o => o.key === props.selectedCategory);
const feedbackService = new FeedbackService();
const [isOpen, setIsOpen] = React.useState(false);
const [isDialogVisible, setIsDialogVisible] = React.useState(false);
const [pageTitle, setPageTitle] = React.useState("");
const [articleInfo, setArticleInfo] = React.useState<IArticleInfo>();
const [txtValue, setTxtValue] = React.useState("");
const [categoryValue, setCategoryValue] = React.useState((defaultCategoryValue) ? defaultCategoryValue.text : "");
React.useEffect(()=>{
feedbackService.getTitle(props.listitemid).then((res) => {
setPageTitle(res);
});
feedbackService.getArticleInfo(props.listitemid).then((res) => {
setArticleInfo(res);
});
}, []);
const openPanel = React.useCallback(() => setIsOpen(true), [isOpen]);
const dismissPanel = React.useCallback(() => setIsOpen(false), [isOpen]);
const hideDialog = React.useCallback(() => setIsDialogVisible(false), [isDialogVisible]);
const hideDialogAndPanel = () => {
setIsOpen(false);
setIsDialogVisible(false);
};
const handleSubmit = (event) => {
event.preventDefault();
let newDefaultCategory;
if (!props.showCategory){
newDefaultCategory = DropdownOptions.Options.find(o => o.key === props.selectedCategory);
}
if (props.listitemid == null){
console.log("List item ID is null. Please run this on a site page.")
}
feedbackService.sendEmailToOwnerGroup(txtValue, props.listitemid, (newDefaultCategory) ? newDefaultCategory.text : categoryValue, props.currentUser.displayName, props.currentUser.email);
dismissPanel();
setIsDialogVisible(true);
setCategoryValue(defaultCategoryValue.text);
setTxtValue("");
};
return (
<div>
<DefaultButton style={{
color: props.themeVariant.semanticColors.buttonText
}} text={unescape(props.buttonLabel)} onClick={openPanel} />
<Panel
isLightDismiss
isOpen={isOpen}
type={PanelType.medium}
onDismiss={dismissPanel}
headerText={strings.PanelHeaderText + pageTitle}
closeButtonAriaLabel="Close"
>
<form onSubmit={handleSubmit}>
<p>{strings.Feedback_Instructions}</p>
<Dropdown label={(props.showCategory) ? strings.FeedbackCategory_Label : ""} options={options} defaultSelectedKey={props.selectedCategory} hidden={(!props.showCategory)}
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => {setCategoryValue(option.text);}}/>
<TextField name="feedbackTxt" multiline rows={8} value={txtValue} label={strings.FeedbackBox_Label}
onChange={(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) =>
{setTxtValue(newValue);}}/>
<br></br>
<div>
<PrimaryButton type="submit" styles={buttonStyles} disabled={(txtValue.length > 10) ? false : true}>
{strings.ButtonText_Submit}
</PrimaryButton>
<DefaultButton onClick={dismissPanel}>{strings.ButtonText_Cancel}</DefaultButton>
</div>
</form>
</Panel>
<Dialog
hidden={!isDialogVisible}
onDismiss={hideDialog}
dialogContentProps={dialogContentProps}
modalProps={dialogModalProps}
>
<DialogFooter>
<PrimaryButton onClick={hideDialogAndPanel} text={strings.ButtonText_Ok} />
</DialogFooter>
</Dialog>
</div>
);
};

View File

@ -0,0 +1,10 @@
import { IReadonlyTheme } from "@microsoft/sp-component-base";
export interface IContainerProps {
buttonLabel: string;
showCategory: boolean;
listitemid: number;
selectedCategory: string;
currentUser: any;
themeVariant: IReadonlyTheme;
}

View File

@ -0,0 +1,18 @@
define([], function() {
return {
"PropertyPane_Description": "Configure feedback properties.",
"PropertyPane_Label_ButtonText": "Button Text",
"PropertyPane_GroupName_Settings": "Settings",
"PropertyPane_GroupName_About": "About Webpart",
"PropertyPane_Label_VersionInfo": "Version: ",
"ButtonText_Cancel": "Cancel",
"ButtonText_Ok": "OK",
"ButtonText_Submit": "Submit",
"PanelHeaderText": "Provide feedback on ",
"FeedbackBox_Label": "Feedback:",
"FeedbackCategory_Label": "Feedback Category:",
"FeedbackCategoryToggle_Label": "Show Feedback Category",
"Feedback_Instructions": "Provide feedback and click the Submit button below."
}
});

View File

@ -0,0 +1,20 @@
declare interface IFeedbackWebPartStrings {
PropertyPane_Description: string;
PropertyPane_Label_ButtonText: string;
PropertyPane_GroupName_Settings: string;
PropertyPane_GroupName_About: string;
PropertyPane_Label_VersionInfo: string;
ButtonText_Cancel: string;
ButtonText_Ok: string;
ButtonText_Submit: string;
PanelHeaderText: string;
FeedbackBox_Label: string;
FeedbackCategory_Label: string;
FeedbackCategoryToggle_Label: string;
Feedback_Instructions: string;
}
declare module 'FeedbackWebPartStrings' {
const strings: IFeedbackWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

40
tsconfig.json Normal file
View File

@ -0,0 +1,40 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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",
"es2017",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"lib"
]
}

30
tslint.json Normal file
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
}
}