New sample 'react-teams-meeting-app-questionnaire'

This commit is contained in:
Nanddeep Nachan 2021-03-22 08:59:53 +00:00
parent 490e66b2a7
commit 57567e033d
40 changed files with 22724 additions and 0 deletions

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.12.0",
"libraryName": "spfx-ms-teams-questionnaire-meeting-app",
"libraryId": "78093b48-2c62-4536-8f93-95ec16b01f8b",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,140 @@
# Microsoft Teams meeting app, Questionnaire with the SharePoint Framework
## Summary
SPFx v1.12 support for Microsoft Teams meeting apps development. Questionnaire meeting app provides Pre-meeting app experience for MS Teams meeting attendees to ask the questions related to meeting before the meeting starts.
![WebPart Preview](./assets/web-part-preview.gif)
The Questionnaire meeting app displays the questions from attendees as pre-meeting app experience.
![Questionnaire Preview](./assets/questionnaire-preview.png)
### NPM Packages Used
Below NPM package(s) are used to develop this sample:
1. @pnp/sp (https://pnp.github.io/pnpjs/sp/)
2. moment (https://www.npmjs.com/package/moment)
### Project setup and important files
```txt
spfx-react-teams-meeting-app-questionnaire
├── teams <-- MS Teams manifest
│ └── manifest.json
└── src
└── models
├── IQuestionnaireItem.ts
└── services
├── SPOService.ts <-- Extensible Service
└── webparts
└── questionnaireMeetingApp
├── QuestionnaireMeetingAppWebPart.manifest.json <-- Configurable web part properties
├── QuestionnaireMeetingAppWebPart.ts
├── components
│ └── QuestionnaireMeetingApp
│ │ ├── QuestionnaireMeetingApp.tsx <-- Questionnaire Component
│ │ ├── QuestionnaireMeetingApp.module.scss
│ │ ├── IQuestionnaireMeetingAppProps.ts
│ │ ├── IQuestionnaireMeetingAppState.ts
│ └── Popup <-- New Question Creation Component
| │ ├── AskQuestion.tsx
| │ ├── IAskQuestionProps.ts
| │ ├── IAskQuestionState.ts
└── loc
├── en-us.js
└── mystrings.d.ts
```
## Used SharePoint Framework Version
![version](https://img.shields.io/badge/version-1.12-green.svg)
## Applies to
- [Microsoft Teams](https://aka.ms/microsoftteams)
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
# WebPart Properties
The properties should be pre-configured inside `QuestionnaireMeetingAppWebPart.manifest.json` as when the web part is added as MS Teams meeting experience, we do not get any settings to configure.
Property|Type|Required|Default value|Comments
--------|----|--------|-------------|--------
siteUrl|Text|Yes|/|Provide the relative URL of the site where below list exists.
listName|Text|Yes|Teams Meeting Questionnaire|Title of the list storing meeting questionnaires.
# SharePoint Asset
A SharePoint list (named `Teams Meeting Questionnaire`) should be manually created to store the meeting questionnaires. The schema of the list is as below.
Display Name|Internal Name|Type|Required|Comments
------------|-------------|----|--------|--------
Title|Title|Single line of text|Y|OOB Title column
Description|Description|Multiple lines of text|N|
MeetingID|MeetingID|Single line of text|N|
## Prerequisites
- Administrative access to MS Teams to deploy the package
# Minimal Path to Awesome
## SharePoint deployment
- Clone this repo
- Navigate to the folder with current sample
- Restore dependencies: `$ npm i`
- Bundle the solution: `$ gulp bundle --ship`
- Package the solution: `$ gulp package-solution --ship`
- Locate the solution at `./sharepoint/solution/spfx-ms-teams-questionnaire-meeting-app.sppkg` and upload it to SharePoint tenant app catalog
![Deploy SPFx solution](./assets/deploy-spfx-solution.png)
- Select `Make this solution available to all sites in the organization`.
- Click `Deploy`.
## MS Teams deployment
- Navigate to `teams` folder and zip the content (2 png files and manifest.json).
- Open MS Teams.
- Click `Apps`.
- Click `Upload a custom app` > `Upload for <tenant>`.
![Deploy to MS Teams](./assets/deploy-to-ms-teams.png)
# Solution
Solution|Author(s)
--------|---------
spfx-react-teams-meeting-app-questionnaire|[Nanddeep Nachan](https://www.linkedin.com/in/nanddeepnachan/) (SharePoint Consultant, [@NanddeepNachan](https://twitter.com/NanddeepNachan))
&nbsp;|[Ravi Kulkarni](https://www.linkedin.com/in/ravi-kulkarni-a5381723/) (SharePoint Consultant, [@RaviKul16a87](https://twitter.com/RaviKul16a87))
&nbsp;|[Smita Nachan](https://www.linkedin.com/in/smitanachan/) (SharePoint Consultant, [@SmitaNachan](https://twitter.com/SmitaNachan))
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|March 22, 2021|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.**
## Features
This project contains sample client-side web part built on the SharePoint Framework illustrating possibilities to surface SPFx web part as Microsoft Teams meeting app.
This sample illustrates the following concepts on top of the SharePoint Framework:
- Surface SPFx web part as Microsoft Teams meeting app
- Using PnP/PnPjs
- Creating extensible services
- Using Office UI Fabric controls for building SharePoint Framework client-side web parts
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
- [PnPjs Configuration](https://pnp.github.io/pnpjs/concepts/configuration/)
- [Support Microsoft Teams Themes in SharePoint Framework Solutions](https://blog.aterentiev.com/support-microsoft-teams-themes-in) by Alex Terentiev, [@alexaterentiev](https://twitter.com/alexaterentiev)

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"questionnaire-meeting-app-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/questionnaireMeetingApp/QuestionnaireMeetingAppWebPart.js",
"manifest": "./src/webparts/questionnaireMeetingApp/QuestionnaireMeetingAppWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"QuestionnaireMeetingAppWebPartStrings": "lib/webparts/questionnaireMeetingApp/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": "spfx-ms-teams-questionnaire-meeting-app",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,21 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "spfx-ms-teams-questionnaire-meeting-app-client-side-solution",
"id": "78093b48-2c62-4536-8f93-95ec16b01f8b",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
}
},
"paths": {
"zippedPackage": "solution/spfx-ms-teams-questionnaire-meeting-app.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,16 @@
'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.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "spfx-ms-teams-questionnaire-meeting-app",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.12.0",
"@microsoft/sp-lodash-subset": "1.12.0",
"@microsoft/sp-office-ui-fabric-core": "1.12.0",
"@microsoft/sp-property-pane": "1.12.0",
"@microsoft/sp-webpart-base": "1.12.0",
"@pnp/sp": "2.3.1-beta0",
"moment": "^2.29.1",
"office-ui-fabric-react": "7.156.0",
"react": "16.9.0",
"react-dom": "16.9.0"
},
"devDependencies": {
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.12.0",
"@microsoft/sp-tslint-rules": "1.12.0",
"@microsoft/sp-module-interfaces": "1.12.0",
"@microsoft/sp-webpart-workbench": "1.12.0",
"@microsoft/rush-stack-compiler-3.7": "0.2.3",
"gulp": "~4.0.2",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1"
}
}

View File

@ -0,0 +1,59 @@
@import "./colors.module";
[data-theme="contrast"] {
:global {
.ms-Fabric {
color: $contrast-primaryText;
}
.ms-Button-icon {
color: $contrast-primaryText;
}
.ms-Overlay {
background-color: $contrast-overlay;
}
.ms-Panel-main {
background-color: $contrast-surfaceBackground;
border-left-color: $contrast-panelBorder;
border-right-color: $contrast-panelBorder;
.ms-Panel-headerText {
color: $contrast-primaryText;
}
}
.spPropertyPaneContainer {
background-color: $contrast-white;
[class^="propertyPane_"] {
background-color: $contrast-white;
border-left-color: $contrast-panelBorder;
[class^="propertyPanePageTitle_"],
[class^="propertyPanePageDescription_"],
[class^="propertyPaneGroupHeaderNoAccordion_"] {
color: $contrast-primaryText;
}
.ms-Button--icon {
&:hover {
background-color: transparent;
}
}
}
}
.ms-Label {
color: $contrast-primaryText;
}
.ms-TextField {
.ms-TextField-fieldGroup {
background-color: $contrast-inputBackground;
color: $contrast-primaryText;
border-color: $contrast-inputBorder;
.ms-TextField-field {
color: $contrast-primaryText;
}
&:hover {
border-color: $contrast-inputBorderHovered;
}
}
}
}
}

View File

@ -0,0 +1,59 @@
@import "./colors.module";
[data-theme="dark"] {
:global {
.ms-Fabric {
color: $dark-primaryText;
}
.ms-Button-icon {
color: $dark-primaryText;
}
.ms-Overlay {
background-color: $dark-overlay;
}
.ms-Panel-main {
background-color: $dark-surfaceBackground;
border-left-color: $dark-panelBorder;
border-right-color: $dark-panelBorder;
.ms-Panel-headerText {
color: $dark-primaryText;
}
}
.spPropertyPaneContainer {
background-color: $dark-white;
[class^="propertyPane_"] {
background-color: $dark-white;
border-left-color: $dark-panelBorder;
[class^="propertyPanePageTitle_"],
[class^="propertyPanePageDescription_"],
[class^="propertyPaneGroupHeaderNoAccordion_"] {
color: $dark-primaryText;
}
.ms-Button--icon {
&:hover {
background-color: transparent;
}
}
}
}
.ms-Label {
color: $dark-primaryText;
}
.ms-TextField {
.ms-TextField-fieldGroup {
background-color: $dark-inputBackground;
color: $dark-primaryText;
border-color: $dark-inputBorder;
.ms-TextField-field {
color: $dark-primaryText;
}
&:hover {
border-color: $dark-inputBorderHovered;
}
}
}
}
}

View File

@ -0,0 +1,61 @@
@import "./colors.module";
[data-theme="default"] {
:global {
.ms-Fabric {
color: $default-primaryText;
}
.ms-Button-icon {
color: $default-primaryText;
}
.ms-Overlay {
background-color: $default-overlay;
}
.ms-Panel-main {
background-color: $default-surfaceBackground;
border-left-color: $default-panelBorder;
border-right-color: $default-panelBorder;
.ms-Panel-headerText {
color: $default-primaryText;
}
}
// Property Pane
.spPropertyPaneContainer {
background-color: $default-white;
[class^="propertyPane_"] {
background-color: $default-white;
border-left-color: $default-panelBorder;
[class^="propertyPanePageTitle_"],
[class^="propertyPanePageDescription_"],
[class^="propertyPaneGroupHeaderNoAccordion_"] {
color: $default-primaryText;
}
.ms-Button--icon {
&:hover {
background-color: transparent;
}
}
}
}
// Text Field
.ms-Label {
color: $default-primaryText;
}
.ms-TextField {
.ms-TextField-fieldGroup {
background-color: $default-inputBackground;
color: $default-primaryText;
border-color: $default-inputBorder;
.ms-TextField-field {
color: $default-primaryText;
}
&:hover {
border-color: $default-inputBorderHovered;
}
}
}
}
}

View File

@ -0,0 +1,61 @@
//SharePoint
$questionnaireMeetingApp-background: "[theme:white, default:#fff]";
$questionnaireMeetingApp-color: "[theme:primaryText, default:#333]";
$questionnaireMeetingAppButton-background: "[theme:themePrimary, default:#0078d4]";
$questionnaireMeetingAppButton-color: "[theme:white, default:#fff]";
$overlay: "[theme:whiteTranslucent40, default:rgba(255, 255,255, 0.4)]";
$surfaceBackground: "[theme:white, default:#fff]";
$primaryText: "[theme:primaryText, default:#333]";
$panelBorder: "[theme: neutralLight, default: #eaeaea]";
$white: "[theme:white, default: #fff]"; // property pane background
$inputBackground: "[theme:inputBackground, default:#fff]"; //input background
$inputBorder: "[theme:inputBorder, default:#a6a6a6]"; // input border
$inputBorderHovered: "[theme:inputBorderHovered, default:#333333]"; // input border hovered
// default theme
$default-questionnaireMeetingApp-background: #f3f2f1;
$default-questionnaireMeetingApp-color: #252423;
$default-questionnaireMeetingAppButton-background: #6264a7;
$default-questionnaireMeetingAppButton-color: #f3f2f1;
$default-overlay: rgba(255, 255, 255, 0.4);
$default-surfaceBackground: #f3f2f1;
$default-primaryText: #252423;
$default-panelBorder: #dedddc;
$default-white: #f3f2f1;
$default-inputBackground: #fff;
$default-inputBorder: #b5b4b2;
$default-inputBorderHovered: #252423;
// dark theme
$dark-questionnaireMeetingApp-background: #2d2c2c;
$dark-questionnaireMeetingApp-color: #ffffff;
$dark-questionnaireMeetingAppButton-background: #6264a7;
$dark-questionnaireMeetingAppButton-color: #2d2c2c;
$dark-overlay: rgba(37, 36, 35, 0.75);
$dark-surfaceBackground: #2d2c2c;
$dark-primaryText: #ffffff;
$dark-panelBorder: #4c4b4b;
$dark-white: #2d2c2c;
$dark-inputBackground: #000;
$dark-inputBorder: #c8c8c8;
$dark-inputBorderHovered: #ffffff;
// contrast theme
$contrast-questionnaireMeetingApp-background: #000000;
$contrast-questionnaireMeetingApp-color: #ffffff;
$contrast-questionnaireMeetingAppButton-background: #6264a7;
$contrast-questionnaireMeetingAppButton-color: #000000;
$contrast-overlay: rgba(37, 36, 35, 0.75);
$contrast-surfaceBackground: #000000;
$contrast-primaryText: #ffffff;
$contrast-panelBorder: #4c4b4b;
$contrast-white: #000000;
$contrast-inputBackground: #000;
$contrast-inputBorder: #c8c8c8;
$contrast-inputBorderHovered: #ffffff;

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,9 @@
// Represents attributes of Questionnaire
export interface IQuestionnaireItem {
ID?: number;
MeetingID: string;
Title: string;
Description: string;
Author?: any;
Modified?: Date;
}

View File

@ -0,0 +1,43 @@
import { IQuestionnaireItem } from "../models/IQuestionnaireItem";
import { sp } from '@pnp/sp/presets/all';
export default class SPOService {
public async getQuestionnaire(listTitle: string, meetingId: string): Promise<IQuestionnaireItem[]> {
let meetingQuestionnaire: IQuestionnaireItem[] = [];
try {
// Get Client POC Master
meetingQuestionnaire = await sp.web.lists.getByTitle(listTitle)
.items
.select("ID,MeetingID,Title,Description,Author/Title,Author/EMail,Modified")
.expand("Author")
.filter(`MeetingID eq '${meetingId}'`)
.orderBy("Modified", false)
.get<IQuestionnaireItem[]>();
}
catch (error) {
console.log(error);
return Promise.reject(error);
}
return meetingQuestionnaire;
}
public async addQuestion(listTitle: string, item: IQuestionnaireItem): Promise<boolean> {
try {
// Get Client POC Master
return sp.web.lists.getByTitle(listTitle)
.items
.add({
Title: item.Title,
Description: item.Description,
MeetingID: item.MeetingID
})
.then((value) => {
return Promise.resolve(true);
});
}
catch (error) {
return Promise.reject(error);
}
}
}

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "31f78b4e-8520-4ef2-884c-fb3bdd5d9985",
"alias": "QuestionnaireMeetingAppWebPart",
"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": "QuestionnaireMeetingApp" },
"description": { "default": "Provides pre-meeting experience with questionnaire" },
"officeFabricIconFontName": "Page",
"properties": {
"siteUrl": "/",
"listName": "Teams Meeting Questionnaire"
}
}]
}

View File

@ -0,0 +1,87 @@
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 'QuestionnaireMeetingAppWebPartStrings';
import QuestionnaireMeetingApp from './components/QuestionnaireMeetingApp';
import { IQuestionnaireMeetingAppProps } from './components/IQuestionnaireMeetingAppProps';
import { sp } from '@pnp/sp/presets/all';
export interface IQuestionnaireMeetingAppWebPartProps {
siteUrl: string;
listName: string;
}
export default class QuestionnaireMeetingAppWebPart extends BaseClientSideWebPart<IQuestionnaireMeetingAppWebPartProps> {
public async onInit(): Promise<void> {
return super.onInit().then(_ => {
if (this.context.sdks.microsoftTeams) {
// checking that we're in Teams
const context = this.context.sdks.microsoftTeams!.context;
this._applyTheme(context.theme || 'default');
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(this._applyTheme);
// Setup context to PnPjs
sp.setup({
spfxContext: this.context,
sp: {
baseUrl: `https://${this.context.sdks.microsoftTeams.context.teamSiteDomain}${this.properties.siteUrl}`
}
});
}
});
}
private _applyTheme = (theme: string): void => {
this.context.domElement.setAttribute('data-theme', theme);
document.body.setAttribute('data-theme', theme);
}
public render(): void {
const element: React.ReactElement<IQuestionnaireMeetingAppProps> = React.createElement(
QuestionnaireMeetingApp,
{
siteUrl: this.properties.siteUrl,
listName: this.properties.listName,
context: this.context
}
);
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('siteUrl', {
label: strings.SiteURLFieldLabel
}),
PropertyPaneTextField('listName', {
label: strings.ListNameFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IQuestionnaireMeetingAppProps {
siteUrl: string;
listName: string;
context: WebPartContext;
}

View File

@ -0,0 +1,7 @@
import { IQuestionnaireItem } from "../../../models/IQuestionnaireItem";
export interface IQuestionnaireMeetingAppState {
infoLoaded: boolean;
meetingQuestionnaire: IQuestionnaireItem[];
showPopup:boolean;
}

View File

@ -0,0 +1,116 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "../../../../common/Global.dark.module.scss";
@import "../../../../common/Global.default.module.scss";
@import "../../../../common/Global.contrast.module.scss";
@import "../../../../common/colors.module";
.askQuestion {
.container {
margin: 0px auto;
}
.labelRequired {
color: red !important;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $default-questionnaireMeetingAppButton-background;
border-color: $default-questionnaireMeetingAppButton-background;
color: $default-questionnaireMeetingAppButton-color;
// 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;
}
}
:global(.ms-Dialog-main) {
// override built-in max-width so dialog can grow wider
max-width: 760px !important;
// 100% width on medium and below
@media (max-width: $ms-screen-max-md) {
& {
min-width: 100%;
}
}
// 80% width on medium to xx-large
@media (min-width: $ms-screen-min-md) and (max-width: $ms-screen-max-xl) {
& {
min-width: 80%;
}
}
// 800px width on x-large and above
@media (min-width: $ms-screen-min-xl) {
& {
min-width: 760px;
}
}
}
}
[data-theme="default"] {
.askQuestion {
background: $default-questionnaireMeetingApp-background;
color: $default-questionnaireMeetingApp-color;
.button {
background: $default-questionnaireMeetingAppButton-background;
background-color: $default-questionnaireMeetingAppButton-background;
color: $default-questionnaireMeetingAppButton-color;
}
}
}
[data-theme="dark"] {
.askQuestion {
background: $dark-questionnaireMeetingApp-background;
color: $dark-questionnaireMeetingApp-color;
.button {
background: $dark-questionnaireMeetingAppButton-background;
background-color: $dark-questionnaireMeetingAppButton-background;
color: $dark-questionnaireMeetingAppButton-color;
}
}
}
[data-theme="contrast"] {
.askQuestion {
background: $contrast-questionnaireMeetingApp-background;
color: $contrast-questionnaireMeetingApp-color;
.button {
background: $contrast-questionnaireMeetingAppButton-background;
background-color: $contrast-questionnaireMeetingAppButton-background;
color: $contrast-questionnaireMeetingAppButton-color;
}
}
}

View File

@ -0,0 +1,105 @@
import * as React from 'react';
import * as strings from 'QuestionnaireMeetingAppWebPartStrings';
import styles from './AskQuestion.module.scss';
import { IAskQuestionProps } from './IAskQuestionProps';
import { IAskQuestionState } from './IAskQuestionState';
import { TextField, Dialog, DialogType, DefaultButton, PrimaryButton, DialogFooter } from 'office-ui-fabric-react';
import SPOService from '../../../../services/SPOService';
import { IQuestionnaireItem } from '../../../../models/IQuestionnaireItem';
export class AskQuestion extends React.Component<IAskQuestionProps, IAskQuestionState> {
private SPOService: SPOService = null;
public constructor(props) {
super(props);
this.state = {
questionTitle: "",
questionDescription: "",
isloading: false,
isSaveClicked: false,
isQuestionTitleEmpty: true
};
this.onSave = this.onSave.bind(this);
this.SPOService = new SPOService();
this.hidePanel = this.hidePanel.bind(this);
}
private hidePanel() {
this.props.onDissmissPanel(true);
}
private async onSave() {
this.setState({ isSaveClicked: true });
if (!this.state.isQuestionTitleEmpty) {
let item: IQuestionnaireItem = {
Title: this.state.questionTitle,
Description: this.state.questionDescription,
MeetingID: this.props.context.sdks.microsoftTeams.context.meetingId
};
this.SPOService.addQuestion(this.props.listName, item)
.then((response: any) => {
this.props.onDissmissPanel(true);
});
}
}
public render(): React.ReactElement<IAskQuestionProps> {
return (
<div className={styles.askQuestion}>
<Dialog
isOpen={this.props.showPopup}
dialogContentProps={{
type: DialogType.normal,
title: strings.AddQuestion,
showCloseButton: true
}}
modalProps={{ containerClassName: styles.askQuestion }}
onDismiss={this.hidePanel}
hidden={false}>
<div>
{
!this.state.isloading &&
<div>
<div>
<TextField
label={strings.Title}
required
deferredValidationTime={500}
onChange={(ev, title) => {
this.setState({ questionTitle: title, isQuestionTitleEmpty: false, isSaveClicked: false });
}}
/>
{this.state.isSaveClicked &&
this.state.questionTitle.trim().length == 0 && (
<div className={styles.labelRequired}>
Client Note Title is required
</div>
)}
</div>
<div>
<TextField
label={strings.Description}
required
multiline
deferredValidationTime={500}
onChange={(ev, addedBy) => {
this.setState({ questionDescription: addedBy, isSaveClicked: false });
}}
/>
</div>
</div>
}
</div>
<DialogFooter>
<PrimaryButton className={styles.button} onClick={this.onSave} text="Save" />
<DefaultButton onClick={this.hidePanel} text="Cancel" />
</DialogFooter>
</Dialog>
</div>
);
}
}

View File

@ -0,0 +1,8 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IAskQuestionProps {
context: WebPartContext;
showPopup: boolean;
onDissmissPanel: (refresh: boolean) => void;
listName: string;
}

View File

@ -0,0 +1,8 @@
import { IDropdownOption } from 'office-ui-fabric-react/';
export interface IAskQuestionState {
questionTitle:string;
questionDescription:string;
isloading:boolean;
isSaveClicked:boolean;
isQuestionTitleEmpty:boolean;
}

View File

@ -0,0 +1,107 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import '../../../common/Global.dark.module.scss';
@import '../../../common/Global.default.module.scss';
@import '../../../common/Global.contrast.module.scss';
@import "../../../common/colors.module";
.questionnaireMeetingApp {
.container {
margin: 0px auto;
}
.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;
}
.subTitle {
@include ms-font-l;
}
.description {
@include ms-font-l;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $default-questionnaireMeetingApp-background;
border-color: $default-questionnaireMeetingApp-background;
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;
}
}
}
[data-theme='default'] {
.questionnaireMeetingApp {
background: $default-questionnaireMeetingApp-background;
color: $default-questionnaireMeetingApp-color;
.button {
background: $default-questionnaireMeetingAppButton-background;
color: $default-questionnaireMeetingAppButton-color;
}
}
}
[data-theme='dark'] {
.questionnaireMeetingApp {
background: $dark-questionnaireMeetingApp-background;
color: $dark-questionnaireMeetingApp-color;
.button {
background: $dark-questionnaireMeetingAppButton-background;
color: $dark-questionnaireMeetingAppButton-color;
}
}
}
[data-theme='contrast'] {
.questionnaireMeetingApp {
background: $contrast-questionnaireMeetingApp-background;
color: $contrast-questionnaireMeetingApp-color;
.button {
background: $contrast-questionnaireMeetingAppButton-background;
color: $contrast-questionnaireMeetingAppButton-color;
}
}
}

View File

@ -0,0 +1,115 @@
import * as React from 'react';
import * as strings from 'QuestionnaireMeetingAppWebPartStrings';
import styles from './QuestionnaireMeetingApp.module.scss';
import { IQuestionnaireMeetingAppProps } from './IQuestionnaireMeetingAppProps';
import { IQuestionnaireMeetingAppState } from './IQuestionnaireMeetingAppState';
import { IQuestionnaireItem } from "../../../models/IQuestionnaireItem";
import SPOService from '../../../services/SPOService';
import { PrimaryButton } from 'office-ui-fabric-react';
import { AskQuestion } from './Popup/AskQuestion';
import { ActivityItem, IActivityItemProps, Link, mergeStyleSets } from 'office-ui-fabric-react';
import * as moment from 'moment';
const classNames = mergeStyleSets({
exampleRoot: {
marginTop: '20px',
},
nameText: {
fontWeight: 'bold',
},
});
export default class QuestionnaireMeetingApp extends React.Component<IQuestionnaireMeetingAppProps, IQuestionnaireMeetingAppState> {
private SPOService: SPOService = null;
public constructor(props) {
super(props);
this.state = {
infoLoaded: false,
meetingQuestionnaire: [],
showPopup: false
};
this.SPOService = new SPOService();
this.onDismissPanel = this.onDismissPanel.bind(this);
}
public async componentDidMount() {
const meetingQuestionnaireInfo: IQuestionnaireItem[] = await this.SPOService.getQuestionnaire(this.props.listName, this.props.context.sdks.microsoftTeams.context.meetingId);
this.setState({
infoLoaded: true,
meetingQuestionnaire: meetingQuestionnaireInfo
});
}
private async onDismissPanel(refresh: boolean) {
this.setState({ showPopup: false, infoLoaded: false });
if (refresh === true) {
const meetingQuestionnaireInfo: IQuestionnaireItem[] = await this.SPOService.getQuestionnaire(this.props.listName, this.props.context.sdks.microsoftTeams.context.meetingId);
this.setState({
infoLoaded: true,
meetingQuestionnaire: meetingQuestionnaireInfo
});
}
}
public render(): React.ReactElement<IQuestionnaireMeetingAppProps> {
return (
<div className={styles.questionnaireMeetingApp}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<PrimaryButton className={styles.button} onClick={() => { this.setState({ showPopup: true }); }} text={strings.AddQuestion} />
</div>
</div>
<div className={styles.row}>
<div className={styles.column}>
<div>
{
this.state.meetingQuestionnaire.map(item => {
const activityItem: (IActivityItemProps & { key: string | number }) = {
key: item.ID,
activityDescription: [
<Link
key={item.ID}
className={classNames.nameText}
>
{item.Author.Title}
</Link>,
<span key={2}> {strings.Posted} </span>,
<span key={3} className={classNames.nameText}>
{item.Title}
</span>,
],
activityPersonas: [{ imageUrl: `/_layouts/15/userphoto.aspx?size=S&username=${item.Author.EMail}` }],
comments: item.Description,
timeStamp: moment(item.Modified).format("LLL"),
};
return (
<ActivityItem {...activityItem} key={activityItem.key} className={classNames.exampleRoot} />
);
})
}
</div>
<div>
{
this.state.showPopup &&
<AskQuestion
onDissmissPanel={this.onDismissPanel}
showPopup={this.state.showPopup}
context={this.props.context}
listName={this.props.listName}
/>
}
</div>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,12 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"SiteURLFieldLabel": "Relative Site URL",
"ListNameFieldLabel": "Relative Site URL",
"AddQuestion": "New Question",
"Posted": "posted",
"Title": "Title",
"Description": "Description"
}
});

View File

@ -0,0 +1,15 @@
declare interface IQuestionnaireMeetingAppWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
SiteURLFieldLabel: string;
ListNameFieldLabel: string;
AddQuestion: string;
Posted: string;
Title: string;
Description: string;
}
declare module 'QuestionnaireMeetingAppWebPartStrings' {
const strings: IQuestionnaireMeetingAppWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

View File

@ -0,0 +1,56 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
"manifestVersion": "1.8",
"version": "1.0.0",
"id": "31f78b4e-8520-4ef2-884c-fb3bdd5d9985",
"packageName": "Meeting Questionnaire",
"developer": {
"name": "Nanddeep Nachan",
"websiteUrl": "https://aka.ms/sppnp",
"privacyUrl": "https://privacy.microsoft.com/privacystatement",
"termsOfUseUrl": "https://www.microsoft.com/servicesagreement"
},
"name": {
"short": "Meeting Questionnaire",
"full": "SPFx based MS Teams Questionnaire Meeting App"
},
"description": {
"short": "MS Teams pre-meeting questionnaire experience",
"full": "Provides MS Teams pre-meeting experience with questionnaire"
},
"icons": {
"outline": "31f78b4e-8520-4ef2-884c-fb3bdd5d9985_outline.png",
"color": "31f78b4e-8520-4ef2-884c-fb3bdd5d9985_color.png"
},
"accentColor": "#004578",
"configurableTabs": [
{
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=31f78b4e-8520-4ef2-884c-fb3bdd5d9985%26forceLocale={locale}",
"canUpdateConfiguration": true,
"scopes": [
"team",
"groupchat"
],
"context": [
"channelTab",
"privateChatTab",
"meetingChatTab",
"meetingDetailsTab",
"meetingSidePanel"
]
}
],
"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

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/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": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "./node_modules/@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
}
}