action handling in aces

This commit is contained in:
Marcin Wojciechowski 2022-05-27 14:52:31 +02:00
parent a980fd6fa4
commit 5b690b9bf9
47 changed files with 25692 additions and 0 deletions

33
samples/ace-strategy-pattern/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
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,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,17 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"version": "1.14.0",
"libraryName": "ace-strategy-pattern",
"libraryId": "7008c810-9588-43c3-b75c-e750cfe57fff",
"environment": "spo",
"packageManager": "npm",
"solutionName": "ace-strategy-pattern",
"solutionShortDescription": "ace-strategy-pattern description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "adaptiveCardExtension",
"aceTemplateType": "Basic"
}
}

View File

@ -0,0 +1,114 @@
# ace-strategy-pattern
## Summary
This sample shows how to avoid conditions hell in quick view actions.
With more complex cards it can get extremely difficult to handle all the actions as adding more and more conditions to target action id impacts code readability.
In this sample we are exposing organization news in adaptive card.
To start we introduce Next and Previous action to navigate between loaded news.
We also want to add possibility to like and comment displayed news.
Commenting is actually a two-action operation as first- we have to show the input and then submit value provided in the input.
Finally we add support for posting this news in a Teams channel - this requires calls to get Teams, then Channels for selected Team and finally post the news in selected channel. This is how it would look like.
![](./doc-resources/QuickViewBeforeRefactoring.PNG)
Note we are already abstracting all the logic in a NewsManager, which hides away significant chunk of the code.
With few simple tricks we can refactor to this
![](./doc-resources/QuickViewAfterRefactoring.PNG)
With a better isolation and testability of our actions.
## Compatibility
![SPFx 1.13.0](https://img.shields.io/badge/SPFx-1.13.0-green.svg)
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
## Prerequisites
None
## Solution
Solution|Author(s)
--------|---------
ace-strategy-pattern | [Marcin Wojciechowski](https://github.com/mgwojciech) [@mgwojciech](https://twitter.com/mgwojciech)
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 27, 2022|Initial commit
## 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 (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/ace-strategy-pattern) then unzip it)
* From your command line, change your current directory to the directory containing this sample (`ace-strategy-pattern`, located under `samples`)
* in the command line run:
* `npm install`
* `gulp serve`
- Add Graph Auto Batching
To run tests
* Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/ace-strategy-pattern) then unzip it)
* From your command line, change your current directory to the directory containing this sample (`ace-strategy-pattern`, located under `samples`)
* in the command line run:
* `npm install`
* `npx jest`
## Features
To understand this sample we need to understand the root problem. First of all, I hope we can all agree, multiple if statements have negative impact on code readability and extensibility. At some point if You need to extend such method You cannot be sure where to put new if statement and it's difficult to controls state changes. Additionally - as it's very difficult to correctly mock objects from SPFx, we hit our test coverage, making actions almost impossible to test, or making our tests performing heavy mocking on SPFx objects (such as QuickView).
To address this we need an interface that can abstract our operations. To perform any action we effectively need only action object, current state and setState method.
We can pass all of this as a method arguments, which we do in src\manager\viewManager\viewActions\IViewActionHandler.ts.
To avoid SPFx objects reference let's create new interface which will represent a contract we have with SPFx Quick view:
src\manager\viewManager\IView.ts
With that in place we can implement a specific ActionHandlers such as
src\manager\viewManager\viewActions\NavigateAction.ts and src\manager\viewManager\viewActions\PostInTeamsAction.ts
Finally we bring it together in src\manager\viewManager\viewActions\ViewActionExecutor.ts where we register actions we want to support in our view.
Everything is handled by src\manager\viewManager\NewsQuickViewManager.ts
Note how now, we can test in isolation sharing news in Teams without any dependency on SPFx
tests\manager\viewManager\viewActions\PostInTeamsAction.test.ts
Hope You'll enjoy it.
> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance.
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
## 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

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"complex-card-adaptive-card-extension": {
"components": [
{
"entrypoint": "./lib/adaptiveCardExtensions/complexCard/ComplexCardAdaptiveCardExtension.js",
"manifest": "./src/adaptiveCardExtensions/complexCard/ComplexCardAdaptiveCardExtension.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ComplexCardAdaptiveCardExtensionStrings": "lib/adaptiveCardExtensions/complexCard/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "ace-strategy-pattern-client-side-solution",
"id": "7008c810-9588-43c3-b75c-e750cfe57fff",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.14.0"
},
"metadata": {
"shortDescription": {
"default": "ace-strategy-pattern description"
},
"longDescription": {
"default": "ace-strategy-pattern description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "ace-strategy-pattern Feature",
"description": "The feature that activates elements of the ace-strategy-pattern solution.",
"id": "828528d0-7849-4e3f-9a1d-e9322e84963e",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/ace-strategy-pattern.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

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,36 @@
{
"name": "ace-strategy-pattern",
"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.14.0",
"@microsoft/sp-property-pane": "1.14.0",
"@microsoft/sp-adaptive-card-extension-base": "1.14.0"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
"@microsoft/sp-build-web": "1.14.0",
"@microsoft/sp-module-interfaces": "1.14.0",
"@microsoft/sp-tslint-rules": "1.14.0",
"@types/chai": "^4.3.1",
"@types/jest": "^27.5.1",
"@types/webpack-env": "1.13.1",
"ajv": "~5.2.2",
"chai": "^4.3.6",
"gulp": "~4.0.2",
"jest": "^27.5.1",
"ts-jest": "^27.1.5"
},
"jest": {
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test))\\.(ts?|tsx?)$"
}
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "28954e80-6946-4786-beea-bf33eca037e1",
"alias": "ComplexCardAdaptiveCardExtension",
"componentType": "AdaptiveCardExtension",
// 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": ["Dashboard"],
"supportsThemeVariants": true,
"preconfiguredEntries": [{
"groupId": "bd067b1e-3ad5-4d5d-a5fe-505f07d7f59c", // Dashboard
"group": { "default": "Dashboard" },
"title": { "default": "Complex Card" },
"description": { "default": "Complex Card description" },
"officeFabricIconFontName": "SharePointLogo",
"properties": {
"title": "Complex Card"
}
}]
}

View File

@ -0,0 +1,80 @@
import { IPropertyPaneConfiguration } from '@microsoft/sp-property-pane';
import { BaseAdaptiveCardExtension } from '@microsoft/sp-adaptive-card-extension-base';
import { CardView } from './cardView/CardView';
import { QuickView } from './quickView/QuickView';
import { ComplexCardPropertyPane } from './ComplexCardPropertyPane';
import { INews, NewsProvider } from '../../dal/NewsProvider';
import { SPFxHttpClient } from '../../dal/http/SPFxHttpClient';
import { SPPFxSPHttpClient } from '../../dal/http/SPPFxSPHttpClient';
import { NewsManager } from "../../manager/NewsManager";
import { IComment, SocialInfoHelper } from '../../dal/SocialInfoHelper';
import { ITeam, ITeamsChannel, TeamsHelper } from '../../dal/TeamsHelper';
export interface IComplexCardAdaptiveCardExtensionProps {
title: string;
}
export interface IComplexCardAdaptiveCardExtensionState {
news: INews[];
selectedNewsIndex: number;
selectedNewsComments?:IComment[];
joinedTeams?: ITeam[];
selectedTeamIndex?: number;
selectedTeamChannels?: ITeamsChannel[];
commentInputVisible?: boolean;
showTeams?: boolean;
showChannels?: boolean;
selectedTeamId?: string;
selectedChannelId?: string;
}
const CARD_VIEW_REGISTRY_ID: string = 'ComplexCard_CARD_VIEW';
export const QUICK_VIEW_REGISTRY_ID: string = 'ComplexCard_QUICK_VIEW';
export default class ComplexCardAdaptiveCardExtension extends BaseAdaptiveCardExtension<
IComplexCardAdaptiveCardExtensionProps,
IComplexCardAdaptiveCardExtensionState
> {
private _deferredPropertyPane: ComplexCardPropertyPane | undefined;
public async onInit(): Promise<void> {
let tempGraphClient = await this.context.aadHttpClientFactory.getClient("https://graph.microsoft.com");
let graphClient = new SPFxHttpClient(tempGraphClient);
let spHttpClient = new SPPFxSPHttpClient(this.context.spHttpClient);
let newsProvider = new NewsProvider(graphClient);
let socialInfoHelper = new SocialInfoHelper(spHttpClient);
let teamsHelper = new TeamsHelper(graphClient);
let newsManager = new NewsManager(newsProvider, socialInfoHelper, teamsHelper);
let news = await newsManager.getNews();
let newsComments = await newsManager.loadComments(news[0]);
this.cardNavigator.register(CARD_VIEW_REGISTRY_ID, () => new CardView());
this.quickViewNavigator.register(QUICK_VIEW_REGISTRY_ID, () => new QuickView(newsManager));
this.state = {
news,
selectedNewsIndex: 0,
selectedNewsComments: newsComments
};
return Promise.resolve();
}
protected loadPropertyPaneResources(): Promise<void> {
return import(
/* webpackChunkName: 'ComplexCard-property-pane'*/
'./ComplexCardPropertyPane'
)
.then(
(component) => {
this._deferredPropertyPane = new component.ComplexCardPropertyPane();
}
);
}
protected renderCard(): string | undefined {
return CARD_VIEW_REGISTRY_ID;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return this._deferredPropertyPane!.getPropertyPaneConfiguration();
}
}

View File

@ -0,0 +1,23 @@
import { IPropertyPaneConfiguration, PropertyPaneTextField } from '@microsoft/sp-property-pane';
import * as strings from 'ComplexCardAdaptiveCardExtensionStrings';
export class ComplexCardPropertyPane {
public getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: { description: strings.PropertyPaneDescription },
groups: [
{
groupFields: [
PropertyPaneTextField('title', {
label: strings.TitleFieldLabel
})
]
}
]
}
]
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,8 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.3125 13.0625C13.9196 13.0625 16.8438 10.1384 16.8438 6.53125C16.8438 2.92414 13.9196 0 10.3125 0C6.70539 0 3.78125 2.92414 3.78125 6.53125C3.78125 10.1384 6.70539 13.0625 10.3125 13.0625Z" fill="#036C70"/>
<path d="M16.5 17.875C19.5376 17.875 22 15.4126 22 12.375C22 9.33743 19.5376 6.875 16.5 6.875C13.4624 6.875 11 9.33743 11 12.375C11 15.4126 13.4624 17.875 16.5 17.875Z" fill="#1A9BA1"/>
<path d="M11 22C13.468 22 15.4688 19.9993 15.4688 17.5312C15.4688 15.0632 13.468 13.0625 11 13.0625C8.53198 13.0625 6.53125 15.0632 6.53125 17.5312C6.53125 19.9993 8.53198 22 11 22Z" fill="#37C6D0"/>
<path opacity="0.5" d="M13.75 5.50464H3.88472C3.82303 5.84353 3.78843 6.1868 3.78125 6.53119C3.78125 8.26338 4.46936 9.92463 5.69421 11.1495C6.91906 12.3743 8.5803 13.0624 10.3125 13.0624C10.5137 13.0624 10.7035 13.021 10.9001 13.0032C10.9031 13.0259 10.9038 13.0493 10.907 13.0718C10.1797 13.0855 9.46672 13.2769 8.83032 13.6293C8.19392 13.9817 7.6534 14.4844 7.25589 15.0937C6.85838 15.703 6.61595 16.4002 6.54975 17.1246C6.48355 17.8491 6.59559 18.5787 6.87609 19.2499H12.3743C12.7356 19.2499 13.0932 19.1788 13.427 19.0406C13.7607 18.9023 14.0639 18.6997 14.3194 18.4443C14.5748 18.1889 14.7774 17.8856 14.9156 17.5519C15.0539 17.2182 15.125 16.8605 15.125 16.4993V6.87964C15.125 6.51497 14.9801 6.16523 14.7223 5.90737C14.4644 5.6495 14.1147 5.50464 13.75 5.50464Z" fill="black"/>
<path d="M12.375 4.125H1.375C0.615608 4.125 0 4.74061 0 5.5V16.5C0 17.2594 0.615608 17.875 1.375 17.875H12.375C13.1344 17.875 13.75 17.2594 13.75 16.5V5.5C13.75 4.74061 13.1344 4.125 12.375 4.125Z" fill="#038387"/>
<path d="M5.07712 10.8554C4.80603 10.6695 4.58101 10.4241 4.41928 10.138C4.26016 9.83564 4.18125 9.49752 4.19007 9.15597C4.17534 8.6949 4.32648 8.24384 4.61603 7.88472C4.91574 7.53092 5.3077 7.26716 5.74833 7.12277C6.24641 6.95324 6.76967 6.8695 7.29578 6.87512C7.98742 6.84915 8.67801 6.94905 9.33393 7.16996V8.65529C9.04944 8.47693 8.73813 8.34545 8.41191 8.26587C8.05945 8.17641 7.69716 8.13156 7.33353 8.13236C6.94967 8.11836 6.5685 8.20157 6.2254 8.37427C6.0939 8.43448 5.98253 8.53131 5.90461 8.65316C5.82669 8.77501 5.78552 8.91673 5.78603 9.06137C5.78391 9.24141 5.85046 9.4155 5.97214 9.54823C6.11501 9.70135 6.28496 9.82675 6.47341 9.9181C6.68388 10.0258 6.99754 10.1686 7.41439 10.3465C7.46064 10.3616 7.50568 10.3802 7.54914 10.4021C7.96131 10.5685 8.35821 10.7705 8.73535 11.0057C9.04484 11.1955 9.29704 11.4657 9.46511 11.7875C9.63317 12.1093 9.7108 12.4707 9.68973 12.8331C9.71074 13.3069 9.57003 13.7738 9.29071 14.1571C9.01622 14.5043 8.64457 14.7618 8.22312 14.897C7.73228 15.056 7.2186 15.133 6.7027 15.1251C6.24096 15.1271 5.77996 15.088 5.32516 15.0082C4.93957 14.9448 4.56359 14.8326 4.20624 14.6745V13.1057C4.54746 13.3572 4.93095 13.5456 5.33855 13.662C5.74129 13.7918 6.16102 13.8611 6.58408 13.8679C6.97637 13.8933 7.36787 13.8075 7.71359 13.6204C7.83325 13.5482 7.93154 13.4455 7.99837 13.3228C8.06519 13.2001 8.09814 13.0618 8.09382 12.9221C8.09573 12.7231 8.01926 12.5312 7.88091 12.3881C7.71009 12.2151 7.51135 12.0722 7.29303 11.9654C7.04326 11.8356 6.67394 11.665 6.18507 11.4536C5.79647 11.2922 5.42526 11.0917 5.07712 10.8554Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,41 @@
import {
BaseBasicCardView,
IBasicCardParameters,
IExternalLinkCardAction,
IQuickViewCardAction,
ICardButton
} from '@microsoft/sp-adaptive-card-extension-base';
import * as strings from 'ComplexCardAdaptiveCardExtensionStrings';
import { IComplexCardAdaptiveCardExtensionProps, IComplexCardAdaptiveCardExtensionState, QUICK_VIEW_REGISTRY_ID } from '../ComplexCardAdaptiveCardExtension';
export class CardView extends BaseBasicCardView<IComplexCardAdaptiveCardExtensionProps, IComplexCardAdaptiveCardExtensionState> {
public get cardButtons(): [ICardButton] | [ICardButton, ICardButton] | undefined {
return [
{
title: strings.QuickViewButton,
action: {
type: 'QuickView',
parameters: {
view: QUICK_VIEW_REGISTRY_ID
}
}
}
];
}
public get data(): IBasicCardParameters {
return {
primaryText: strings.PrimaryText,
title: this.properties.title
};
}
public get onCardSelection(): IQuickViewCardAction | IExternalLinkCardAction | undefined {
return {
type: 'ExternalLink',
parameters: {
target: 'https://www.bing.com'
}
};
}
}

View File

@ -0,0 +1,11 @@
define([], function() {
return {
"PropertyPaneDescription": "Write 1-3 sentences describing the functionality of this component.",
"TitleFieldLabel": "Card title",
"Title": "Adaptive Card Extension",
"SubTitle": "Quick view",
"PrimaryText": "SPFx Adaptive Card Extension",
"Description": "Create your SPFx Adaptive Card Extension solution!",
"QuickViewButton": "Quick view"
}
});

View File

@ -0,0 +1,14 @@
declare interface IComplexCardAdaptiveCardExtensionStrings {
PropertyPaneDescription: string;
TitleFieldLabel: string;
Title: string;
SubTitle: string;
PrimaryText: string;
Description: string;
QuickViewButton: string;
}
declare module 'ComplexCardAdaptiveCardExtensionStrings' {
const strings: IComplexCardAdaptiveCardExtensionStrings;
export = strings;
}

View File

@ -0,0 +1,56 @@
import { ISPFxAdaptiveCard, BaseAdaptiveCardView, IActionArguments } from '@microsoft/sp-adaptive-card-extension-base';
import * as strings from 'ComplexCardAdaptiveCardExtensionStrings';
import { IComment } from '../../../dal/SocialInfoHelper';
import { NewsManager } from '../../../manager/NewsManager';
import { INewsView } from '../../../manager/viewManager/INewsView';
import { NewsQuickViewManager } from '../../../manager/viewManager/NewsQuickViewManager';
import { IComplexCardAdaptiveCardExtensionProps, IComplexCardAdaptiveCardExtensionState } from '../ComplexCardAdaptiveCardExtension';
export interface IQuickViewData extends IComplexCardAdaptiveCardExtensionState {
subTitle: string;
title: string;
description: string;
newsThumbnail: string;
joinedTeamsOptions: {title: string, value: string}[];
channelsOptions: {title: string, value: string}[];
}
export class QuickView extends BaseAdaptiveCardView<
IComplexCardAdaptiveCardExtensionProps,
IComplexCardAdaptiveCardExtensionState,
IQuickViewData
> implements INewsView {
protected viewManager: NewsQuickViewManager;
constructor(protected newsManager: NewsManager) {
super();
this.viewManager = new NewsQuickViewManager(newsManager);
}
public get data(): IQuickViewData {
let news = this.state.news[this.state.selectedNewsIndex];
let joinedTeamsOptions = this.state.joinedTeams ? this.state.joinedTeams.map((team)=>({
title: team.displayName,
value: team.id
})) : [];
let channelsOptions = this.state.selectedTeamChannels ? this.state.selectedTeamChannels.map((team)=>({
title: team.displayName,
value: team.id
})) : [];
return {
...this.state,
subTitle: `By ${news.author} on ${(new Date(news.firstPublishedDateOWSDATE)).toLocaleDateString()}`,
title: `${news.title} (${this.state.selectedNewsIndex + 1}/${this.state.news.length})`,
description: news.description,
newsThumbnail: news.pictureThumbnailURL,
joinedTeamsOptions,
channelsOptions
};
}
public get template(): ISPFxAdaptiveCard {
return require('./template/QuickViewTemplate.json');
}
public onAction(action: IActionArguments): void {
this.viewManager.handleAction(action as any, this);
}
}

View File

@ -0,0 +1,103 @@
import { ISPFxAdaptiveCard, BaseAdaptiveCardView, IActionArguments } from '@microsoft/sp-adaptive-card-extension-base';
import * as strings from 'ComplexCardAdaptiveCardExtensionStrings';
import { IComment } from '../../../dal/SocialInfoHelper';
import { NewsManager } from '../../../manager/NewsManager';
import { IComplexCardAdaptiveCardExtensionProps, IComplexCardAdaptiveCardExtensionState } from '../ComplexCardAdaptiveCardExtension';
export interface IQuickViewData extends IComplexCardAdaptiveCardExtensionState {
subTitle: string;
title: string;
description: string;
newsThumbnail: string;
joinedTeamsOptions: {title: string, value: string}[];
channelsOptions: {title: string, value: string}[];
}
export class QuickViewBeforeRefactoring extends BaseAdaptiveCardView<
IComplexCardAdaptiveCardExtensionProps,
IComplexCardAdaptiveCardExtensionState,
IQuickViewData
> {
constructor(protected newsManager: NewsManager) {
super();
}
public get data(): IQuickViewData {
let news = this.state.news[this.state.selectedNewsIndex];
let joinedTeamsOptions = this.state.joinedTeams ? this.state.joinedTeams.map((team)=>({
title: team.displayName,
value: team.id
})) : [];
let channelsOptions = this.state.selectedTeamChannels ? this.state.selectedTeamChannels.map((team)=>({
title: team.displayName,
value: team.id
})) : [];
return {
...this.state,
subTitle: `By ${news.author} on ${(new Date(news.firstPublishedDateOWSDATE)).toLocaleDateString()}`,
title: `${news.title} (${this.state.selectedNewsIndex + 1}/${this.state.news.length})`,
description: news.description,
newsThumbnail: news.pictureThumbnailURL,
joinedTeamsOptions,
channelsOptions
};
}
public get template(): ISPFxAdaptiveCard {
return require('./template/QuickViewTemplate.json');
}
public onAction(action: IActionArguments): void {
if (action.id === "next") {
let newIndex = (this.state.selectedNewsIndex + 1) % this.state.news.length;
this.setState({ selectedNewsIndex: newIndex, selectedNewsComments:[] });
this.newsManager.loadComments(this.state.news[newIndex]).then((comments) => {
this.setState({ selectedNewsComments: comments });
});
}
if (action.id === "previous") {
let newIndex = (this.state.selectedNewsIndex - 1) % this.state.news.length;
this.setState({ selectedNewsIndex: newIndex, selectedNewsComments:[] });
this.newsManager.loadComments(this.state.news[newIndex]).then((comments) => {
this.setState({ selectedNewsComments: comments });
});
}
if(action.id === "showAddCommentInput"){
this.setState({commentInputVisible: true});
}
if(action.id === "addComment"){
let commentText = (action as any).data.newCommentInput;
this.newsManager.commentNews(this.state.news[this.state.selectedNewsIndex], commentText).then(()=>{
let comments = this.state.selectedNewsComments;
comments.unshift({
author:{
name: "You"
} as any,
text: commentText
})
this.setState({selectedNewsComments:comments, commentInputVisible: false});
});
}
if(action.id === "likePost"){
this.newsManager.likeNews(this.state.news[this.state.selectedNewsIndex]).then(()=>{
});
}
if(action.id === "loadTeams"){
this.newsManager.getJoinedTeams().then((teams)=>{
this.setState({joinedTeams:teams, showTeams: true});
});
}
if(action.id === "showSelectChannel"){
let teamId = (action as any).data.selectTeamsDD;
this.newsManager.getChannels(teamId).then((channels)=>{
this.setState({selectedTeamChannels: channels, showChannels: true, selectedTeamId: teamId});
});
}
if(action.id === "shareInSelectedChannel"){
let channelId = (action as any).data.selectChannelDD;
this.newsManager.shareNews(this.state.news[this.state.selectedNewsIndex], this.state.selectedTeamId, channelId).then(()=>{
this.setState({selectedTeamId: null,selectedTeamChannels: [], showTeams: false, showChannels: false});
});
}
}
}

View File

@ -0,0 +1,155 @@
{
"schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${title}"
},
{
"type": "Image",
"size": "Stretch",
"url": "${newsThumbnail}"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "${subTitle}",
"wrap": true
}
]
}
]
},
{
"type": "TextBlock",
"text": "${description}",
"wrap": true
},
{
"type": "ColumnSet",
"$data": "${selectedNewsComments}",
"columns": [
{
"type": "Column",
"width": 40,
"items": [
{
"type": "TextBlock",
"text": "${author.name}",
"wrap": true,
"size": "Small",
"isSubtle": true
}
]
},
{
"type": "Column",
"width": 50,
"items": [
{
"type": "TextBlock",
"text": "${text}",
"wrap": true,
"size": "Default"
}
]
}
],
"separator": true
},
{
"type": "Container",
"$when": "${commentInputVisible == true}",
"items": [
{
"type": "Input.Text",
"placeholder": "Your comment",
"label":"Comment",
"errorMessage": "Please enter comment",
"id":"newCommentInput",
"isRequired": true,
"inlineAction": {
"type": "Action.Submit",
"title": "Add",
"id":"addComment"
}
}
]
},
{
"type": "Input.ChoiceSet",
"$when": "${showTeams == true}",
"id": "selectTeamsDD",
"choices": "${joinedTeamsOptions}",
"placeholder": "Select a team",
"value":"${selectedTeamId}",
"separator": true,
"wrap": true
},
{
"type": "Input.ChoiceSet",
"$when": "${showChannels == true}",
"id": "selectChannelDD",
"choices": "${channelsOptions}",
"placeholder": "Select a team",
"separator": true,
"wrap": true
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Share in selected chanel",
"$when": "${showChannels == true}",
"id": "shareInSelectedChannel"
},
{
"type": "Action.Submit",
"title": "Select channel",
"$when": "${showTeams == true}",
"id": "showSelectChannel"
},
{
"type": "Action.Submit",
"title": "Like this post",
"$when": "${showTeams != true}",
"id": "likePost",
"data": "likePost"
},
{
"type": "Action.Submit",
"$when": "${showAddCommentInput != true && showTeams != true}",
"title": "Add a comment",
"id": "showAddCommentInput",
"data": "showAddCommentInput"
},
{
"type": "Action.Submit",
"title": "Share in teams",
"$when": "${showTeams != true}",
"id": "loadTeams",
"data": "loadTeams"
},
{
"type": "Action.Submit",
"title": "Previous",
"id": "previous",
"data":"previous"
},
{
"type": "Action.Submit",
"title": "Next",
"id": "next",
"data": "Next"
}
]
}

View File

@ -0,0 +1,68 @@
import { IHttpClient } from "./http/IHttpClient";
export interface INews {
author: string;
description: string;
listID: string;
listItemID: string;
path: string;
pictureThumbnailURL: string;
spWebUrl: string;
title: string;
uniqueId: string;
firstPublishedDateOWSDATE: string;
}
export class NewsProvider {
protected baseQuery = "ContentTypeId:0x0101009D1CB255DA76424F860D91F20E6C4118* AND PromotedState:2";
public static readonly baseMaxResults = 32;
protected readonly selectedField = ["Title", "Description", "PictureThumbnailURL", "Path", "UniqueId", "SPWebUrl", "ListItemID", "ListID", "Author", "FirstPublishedDateOWSDATE"];
constructor(protected graphClient: IHttpClient, public maxResults?: number, public query?: string) {
this.query = query || this.baseQuery;
this.maxResults = maxResults || NewsProvider.baseMaxResults;
}
public pollData(lastPoll: Date): Promise<any[]> {
return this.getData();
}
public getDataById(id: any): Promise<any> {
return;
}
public async getData(): Promise<INews[]> {
try {
let searchResults = await this.graphClient.post("https://graph.microsoft.com/v1.0/search/query", {
body: JSON.stringify({
"requests": [
{
"entityTypes": [
"listItem"
],
"query": {
"queryString": `${this.query}`
},
"fields": this.selectedField
}
]
})
});
if (searchResults.ok) {
let results = await searchResults.json();
return this.parseNews(results.value[0].hitsContainers[0].hits.map(hit => hit.resource.fields));
}
}
catch (err) {
throw err;
}
}
protected parseNews(news: any): INews[] {
return news.sort((a: INews, b: INews) => {
return a.firstPublishedDateOWSDATE < b.firstPublishedDateOWSDATE
? 1
: a.firstPublishedDateOWSDATE === b.firstPublishedDateOWSDATE
? 0
: -1;
});
}
}

View File

@ -0,0 +1,30 @@
import { IHttpClient } from "./http/IHttpClient";
import { INews } from "./NewsProvider";
export interface IComment{
text: string;
author: {
email: string;
name: string;
}
}
export class SocialInfoHelper {
constructor(protected spHttpClient: IHttpClient) {
}
public async likeNews(news: INews): Promise<void> {
await this.spHttpClient.post(`${news.spWebUrl}/_api/web/lists('${news.listID}')/GetItemById(${news.listItemID})/like`, { body: "{}" });
}
public async commentNews(news: INews, comment: string): Promise<void> {
await this.spHttpClient.post(`${news.spWebUrl}/_api/web/lists('${news.listID}')/GetItemById(${news.listItemID})/GetComments`, {
body: JSON.stringify({
text: comment
})
});
}
public async loadComments(news: INews): Promise<IComment[]> {
let response = await this.spHttpClient.get(`${news.spWebUrl}/_api/web/lists('${news.listID}')/GetItemById(${news.listItemID})/GetComments`);
return (await response.json()).value;
}
}

View File

@ -0,0 +1,51 @@
import { Guid } from "@microsoft/sp-core-library";
import { IHttpClient } from "./http/IHttpClient";
export interface ITeam{
id: string;
displayName: string;
}
export interface ITeamsChannel{
id: string;
displayName: string;
}
export class TeamsHelper{
constructor(protected graphClient:IHttpClient){
}
public async getTeams():Promise<ITeam[]>{
let response = await this.graphClient.get("https://graph.microsoft.com/v1.0/me/joinedTeams");
if(response.ok){
return (await response.json()).value;
}
else throw new Error("Error getting teams, " + response.statusText);
}
public async getTeamsChannels(teamId: string):Promise<ITeamsChannel[]>{
let response = await this.graphClient.get(`https://graph.microsoft.com/v1.0/teams/${teamId}/channels`);
if(response.ok){
return (await response.json()).value;
}
else throw new Error("Error getting teams, " + response.statusText);
}
public async postNewsCard(newsCard: any, teamId: string, channelId: string){
let attachmentId = Guid.newGuid().toString();
let messageBody = {
subject: newsCard.title,
body:{
contentType:"html",
content: `<attachment id="${attachmentId}"></attachment>`
},
attachments:[{
id: attachmentId,
contentType: "application/vnd.microsoft.card.adaptive",
contentUrl: null,
content: JSON.stringify(newsCard)
}]
}
return await this.graphClient.post(`https://graph.microsoft.com/v1.0/teams/${teamId}/channels/${channelId}/messages`, {
body: JSON.stringify(messageBody)
});
}
}

View File

@ -0,0 +1,16 @@
export interface IHttpClientResponse {
json: () => Promise<any>;
text: () => Promise<string>;
blob: () => Promise<Blob>;
ok: boolean;
status: number;
statusText?: string;
}
export interface IHttpClient {
get(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
post(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
patch(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
put(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
delete(url: string): Promise<IHttpClientResponse>;
}

View File

@ -0,0 +1,60 @@
import { IHttpClient, IHttpClientResponse } from "./IHttpClient";
import { AadHttpClient } from "@microsoft/sp-http";
export class SPFxHttpClient implements IHttpClient {
constructor(protected httpClient: AadHttpClient) {
}
public get(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.get(url, AadHttpClient.configurations.v1, {
...options,
headers: {
...options?.headers,
Accept: "application/json",
ConsistencyLevel: "eventual",
"Content-Type": "application/json"
}
});
}
public post(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.post(url, AadHttpClient.configurations.v1, {
...options,
headers: {
...options?.headers,
Accept: "application/json",
ConsistencyLevel: "eventual",
"Content-Type": "application/json"
}
});
}
public patch(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
...options,
headers: {
...options?.headers,
Accept: "application/json",
ConsistencyLevel: "eventual",
"Content-Type": "application/json"
},
method: "PATCH"
});
}
public put(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
...options,
headers: {
...options?.headers,
Accept: "application/json",
ConsistencyLevel: "eventual",
"Content-Type": "application/json"
},
method: "PUT"
});
}
public delete(url: string): Promise<IHttpClientResponse> {
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
method: "DELETE"
});
}
}

View File

@ -0,0 +1,43 @@
import { IHttpClient, IHttpClientResponse } from "./IHttpClient";
import { SPHttpClient } from "@microsoft/sp-http";
export class SPPFxSPHttpClient implements IHttpClient {
constructor(protected httpClient: SPHttpClient) {
}
public get(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.get(url, SPHttpClient.configurations.v1, {
...options,
headers: {
...options?.headers,
}
});
}
public post(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.post(url, SPHttpClient.configurations.v1, options);
}
public patch(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.fetch(url, SPHttpClient.configurations.v1, {
...options,
headers: {
...options?.headers,
},
method: "PATCH"
});
}
public put(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.httpClient.fetch(url, SPHttpClient.configurations.v1, {
...options,
headers: {
...options?.headers,
},
method: "PUT"
});
}
public delete(url: string): Promise<IHttpClientResponse> {
return this.httpClient.fetch(url, SPHttpClient.configurations.v1, {
method: "DELETE"
});
}
}

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,68 @@
import { IHttpClient } from "../dal/http/IHttpClient";
import { INews, NewsProvider } from "../dal/NewsProvider";
import { IComment, SocialInfoHelper } from "../dal/SocialInfoHelper";
import { ITeam, ITeamsChannel, TeamsHelper } from "../dal/TeamsHelper";
export class NewsManager{
constructor(protected newsProvider: NewsProvider, protected socialInfoHelper: SocialInfoHelper, protected teamsHelper: TeamsHelper){
}
public async getNews(): Promise<INews[]>{
return this.newsProvider.getData();
}
public async likeNews(news: INews): Promise<void>{
await this.socialInfoHelper.likeNews(news);
}
public async commentNews(news: INews, comment: string): Promise<void>{
await this.socialInfoHelper.commentNews(news, comment);
}
public async loadComments(news:INews):Promise<IComment[]>{
return this.socialInfoHelper.loadComments(news);
}
public async getJoinedTeams():Promise<ITeam[]>{
return this.teamsHelper.getTeams();
}
public async getChannels(teamId:string):Promise<ITeamsChannel[]>{
return this.teamsHelper.getTeamsChannels(teamId);
}
public async shareNews(news:INews, teamId:string, channelId:string):Promise<void>{
let newsACE = {
"schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": news.title
},
{
"type": "Image",
"size": "Stretch",
"url": news.pictureThumbnailURL
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": `By ${news.author} on ${(new Date(news.firstPublishedDateOWSDATE)).toLocaleDateString()}`,
"wrap": true
}
]
}
]
},
{
"type": "TextBlock",
"text": news.description,
"wrap": true
}]
}
await this.teamsHelper.postNewsCard(newsACE, teamId, channelId);
}
}

View File

@ -0,0 +1,5 @@
import { IComplexCardAdaptiveCardExtensionState } from "../../adaptiveCardExtensions/complexCard/ComplexCardAdaptiveCardExtension";
import { IView } from "./IView";
export interface INewsView extends IView<IComplexCardAdaptiveCardExtensionState> {
}

View File

@ -0,0 +1,4 @@
export interface IView<T>{
state: T;
setState(state: Partial<T>): void;
}

View File

@ -0,0 +1,14 @@
import { NewsManager } from "../NewsManager";
import { INewsView } from "./INewsView";
import { ViewActionExecutor } from "./viewActions/ViewActionExecutor";
export class NewsQuickViewManager{
protected actionExecutor: ViewActionExecutor;
constructor(protected newsManger: NewsManager){
this.actionExecutor = new ViewActionExecutor(this.newsManger);
}
public handleAction(action: {id: string, data?: any}, quickView: INewsView){
this.actionExecutor.handleAction(action, quickView);
}
}

View File

@ -0,0 +1,30 @@
import { NewsManager } from "../../NewsManager";
import { INewsView } from "../INewsView";
import { IViewActionHandler } from "./IViewActionHandler";
export class AddCommentAction implements IViewActionHandler{
constructor(protected newsManager: NewsManager) {
}
shouldHandleAction(action: { id: string; data?: any; }): boolean {
return action.id === "showAddCommentInput" || action.id === "addComment";
}
handleAction(action: { id: string; data?: any; }, quickView: INewsView): void {
if(action.id === "showAddCommentInput"){
quickView.setState({commentInputVisible: true});
}
if(action.id === "addComment"){
let commentText = (action as any).data.newCommentInput;
this.newsManager.commentNews(quickView.state.news[quickView.state.selectedNewsIndex], commentText).then(()=>{
let comments = quickView.state.selectedNewsComments;
comments.unshift({
author:{
name: "You"
} as any,
text: commentText
})
quickView.setState({selectedNewsComments:comments, commentInputVisible: false});
});
}
}
}

View File

@ -0,0 +1,6 @@
import { INewsView } from "../INewsView";
export interface IViewActionHandler {
shouldHandleAction(action: { id: string, data?: any }): boolean;
handleAction(action: { id: string, data?: any }, quickView: INewsView): void;
}

View File

@ -0,0 +1,16 @@
import { NewsManager } from "../../NewsManager";
import { INewsView } from "../INewsView";
import { IViewActionHandler } from "./IViewActionHandler";
export class LikePostAction implements IViewActionHandler{
constructor(protected newsManager: NewsManager) {
}
shouldHandleAction(action: { id: string; data?: any; }): boolean {
return action.id === "likePost";
}
handleAction(action: { id: string; data?: any; }, quickView: INewsView): void {
this.newsManager.likeNews(quickView.state.news[quickView.state.selectedNewsIndex]).then(()=>{
});
}
}

View File

@ -0,0 +1,19 @@
import { NewsManager } from "../../NewsManager";
import { INewsView } from "../INewsView";
import { IViewActionHandler } from "./IViewActionHandler";
export class NavigateAction implements IViewActionHandler {
constructor(protected newsManager: NewsManager) {
}
shouldHandleAction(action: { id: string; data?: any; }): boolean {
return action.id === "next" || action.id === "previous";
}
handleAction(action: { id: string; data?: any; }, quickView: INewsView): void {
let newIndex = action.id === "next" ? (quickView.state.selectedNewsIndex + 1) % quickView.state.news.length : (quickView.state.selectedNewsIndex - 1) % quickView.state.news.length;
quickView.setState({ selectedNewsIndex: newIndex, selectedNewsComments: [] });
this.newsManager.loadComments(quickView.state.news[newIndex]).then((comments) => {
quickView.setState({ selectedNewsComments: comments });
});
}
}

View File

@ -0,0 +1,31 @@
import { NewsManager } from "../../NewsManager";
import { INewsView } from "../INewsView";
import { IViewActionHandler } from "./IViewActionHandler";
export class PostInTeamsAction implements IViewActionHandler {
constructor(protected newsManager: NewsManager) {
}
shouldHandleAction(action: { id: string; data?: any; }): boolean {
return action.id === "loadTeams" || action.id === "showSelectChannel" || action.id === "shareInSelectedChannel";
}
handleAction(action: { id: string; data?: any; }, quickView: INewsView): void {
if (action.id === "loadTeams") {
this.newsManager.getJoinedTeams().then((teams) => {
quickView.setState({ joinedTeams: teams, showTeams: true });
});
}
if (action.id === "showSelectChannel") {
let teamId = (action as any).data.selectTeamsDD;
this.newsManager.getChannels(teamId).then((channels) => {
quickView.setState({ selectedTeamChannels: channels, showChannels: true, selectedTeamId: teamId });
});
}
if (action.id === "shareInSelectedChannel") {
let channelId = (action as any).data.selectChannelDD;
this.newsManager.shareNews(quickView.state.news[quickView.state.selectedNewsIndex], quickView.state.selectedTeamId, channelId).then(() => {
quickView.setState({ selectedTeamId: null, selectedTeamChannels: [], showTeams: false, showChannels: false });
});
}
}
}

View File

@ -0,0 +1,25 @@
import { INewsView } from "../INewsView";
import { AddCommentAction } from "./AddCommentAction";
import { IViewActionHandler } from "./IViewActionHandler";
import { LikePostAction } from "./LikePostAction";
import { NavigateAction } from "./NavigateAction";
import { PostInTeamsAction } from "./PostInTeamsAction";
export class ViewActionExecutor {
public actions: IViewActionHandler[];
constructor(newsManager) {
this.actions = [
new NavigateAction(newsManager),
new AddCommentAction(newsManager),
new LikePostAction(newsManager),
new PostInTeamsAction(newsManager)
];
}
public handleAction(action: { id: string; data?: any; }, quickView: INewsView): void {
this.actions.forEach((handler) => {
if (handler.shouldHandleAction(action)) {
handler.handleAction(action, quickView);
}
});
}
}

View File

@ -0,0 +1,286 @@
///<reference types="jest" />
import { NewsProvider } from "../../src/dal/NewsProvider";
import { MockGraphClient } from "../mocks/MockGraphClient";
describe("NewsProvider", () => {
it("should return news", async () => {
let graphClient = new MockGraphClient();
graphClient.responses.set("https://graph.microsoft.com/v1.0/search/query",{
"value": [
{
"searchTerms": [],
"hitsContainers": [
{
"hits": [
{
"hitId": "66A0EED6-FA43-49FD-AA03-EF82B4040140",
"rank": 1,
"summary": " daf0b71c-6de8-4ef7-b511-faae7c388708 Another post<ddd/>Another post<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test Post",
"description": "Another post",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/images/sitepagethumbnail.png",
"path": "https://test.sharepoint.com/SitePages/Test-Post.aspx",
"uniqueId": "{66A0EED6-FA43-49FD-AA03-EF82B4040140}",
"spWebUrl": "https://test.sharepoint.com",
"listItemID": "15",
"listID": "274163ca-a930-455a-9e14-40906b4edd5d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2021-06-15T12:52:29Z"
}
}
},
{
"hitId": "B4FDA1D9-F775-48AC-AC81-31F6795223EA",
"rank": 2,
"summary": "",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test news (review) 15-05-2020",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/images/sitepagethumbnail.png",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Test-news-(review)-15-05-2020.aspx",
"uniqueId": "{B4FDA1D9-F775-48AC-AC81-31F6795223EA}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "17",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-05-15T11:23:19Z"
}
}
},
{
"hitId": "F20028EB-924F-4DAE-A263-82C75D33B7EE",
"rank": 3,
"summary": " How do you get started? Select 'Edit' to start working with this basic two-column template with an emphasis on text and examples of text formatting. With your page in edit mode, <ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "News from Test team",
"description": "How do you get started? Select 'Edit' to start working with this basic two-column template with an emphasis on text and examples of text formatting. With your page in edit mode, select this paragraph and replace it with your own text. Then, select t…",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/images/sitepagethumbnail.png",
"path": "https://test.sharepoint.com/sites/Testteam646/SitePages/News-from-Test-team.aspx",
"uniqueId": "{F20028EB-924F-4DAE-A263-82C75D33B7EE}",
"spWebUrl": "https://test.sharepoint.com/sites/Testteam646",
"listItemID": "2",
"listID": "cfb20819-517b-4f63-96b9-6fd954f22933",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-04-23T07:32:58Z"
}
}
},
{
"hitId": "62EF2409-F0E9-457B-810F-75165D1228B6",
"rank": 4,
"summary": " Some test content of my test news<ddd/>Some test content of my test news<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Some test News",
"description": "Some test content of my test news",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/getpreview.ashx?guidSite=6939921445424f93b658328a712a834f&guidWeb=f5ad2fe7d9f14b968dce884fa6cc49cd&guidFile=1b6898259ab84db389681cdffce4d117&ext=jpeg",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Some-test-News.aspx",
"uniqueId": "{62EF2409-F0E9-457B-810F-75165D1228B6}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "100",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2021-03-25T10:05:21Z"
}
}
},
{
"hitId": "92B0C0A1-BF46-4345-AD62-9A86ABE6BFFB",
"rank": 5,
"summary": " Publish something about CSGO. Today for the very first time we will be joined by my good friend Mateusz! e84a8ca2-f63c-4fb9-bc0b-d8eef5ccb22b<ddd/>Publish something about CS:GO. Today<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test-30-10-2020-2",
"description": "Publish something about CS:GO. Today for the very first time we will be joined by my good friend Mateusz!",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/getpreview.ashx?guidSite=6939921445424f93b658328a712a834f&guidWeb=f5ad2fe7d9f14b968dce884fa6cc49cd&guidFile=51b1a4ea93664b56aa174ce2a039ebea&ext=jpeg",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Test-30-10-2020-2.aspx",
"uniqueId": "{92B0C0A1-BF46-4345-AD62-9A86ABE6BFFB}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "80",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-10-30T12:26:47Z"
}
}
},
{
"hitId": "86FDD130-9219-4C4E-A3F5-63CB1C14A216",
"rank": 6,
"summary": " I'm going to play BG again. I already started last weekend and its amazing. Hope I'll get few hours this weekend as well. Just between You and me, I'm thinking about blowing<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test-30-10-2020-1",
"description": "I'm going to play BG again. I already started last weekend and its amazing. Hope I'll get few hours this weekend as well. Just between You and me, I'm thinking about blowing todays CS for BG...difficult decisions ahead.",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/getpreview.ashx?guidSite=6939921445424f93b658328a712a834f&guidWeb=f5ad2fe7d9f14b968dce884fa6cc49cd&guidFile=bc31bba470754c1aaa91d0a85d08df4a&ext=jpeg",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Test-30-10-2020-1.aspx",
"uniqueId": "{86FDD130-9219-4C4E-A3F5-63CB1C14A216}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "79",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-10-30T08:40:30Z"
}
}
},
{
"hitId": "B5EB893D-CD84-4029-B323-95D2B5594C53",
"rank": 7,
"summary": " Some test news content. Another paragraph. And one more, just for fun. And one more to check if label is passed<ddd/>Some test news content. Another paragraph. And one more, just for<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test-12-11-2020-1",
"description": "Some test news content. Another paragraph. And one more, just for fun. And one more to check if label is passed",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/getpreview.ashx?guidSite=6939921445424f93b658328a712a834f&guidWeb=f5ad2fe7d9f14b968dce884fa6cc49cd&guidFile=5800091587144706be5dc504509f80f9&ext=jpeg",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Test-12-11-2020-1.aspx",
"uniqueId": "{B5EB893D-CD84-4029-B323-95D2B5594C53}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "83",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-11-12T06:53:16Z"
}
}
},
{
"hitId": "4F5CC501-FD89-46AE-A48B-158FAC9B96B6",
"rank": 8,
"summary": " Hm...that might be a time to play sc again. <ddd/>Hm...that might be a time to play sc again.<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test-17-11-2020-1",
"description": "Hm...that might be a time to play sc again.",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/getpreview.ashx?guidSite=9d181eecec514fc0b897d638bb843e3a&guidWeb=f5ad2fe7d9f14b968dce884fa6cc49cd&guidFile=1e6009a7d2d34e758bd439a8c33282ca&ext=jpeg",
"path": "https://test.sharepoint.com/sites/tea-point-about-us/SitePages/news-Test-17-11-2020-1.aspx",
"uniqueId": "{4F5CC501-FD89-46AE-A48B-158FAC9B96B6}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point-about-us",
"listItemID": "27",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-11-17T10:40:44Z"
}
}
},
{
"hitId": "591442E1-B64D-46C9-81CF-4672C63A5D8C",
"rank": 9,
"summary": " 6409567c-ede1-4834-af41-55ca074c4048 0767823f-7fc4-48f8-9e92-3678d353f033<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test-22-4-2020-NotHighlighted",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/images/sitepagethumbnail.png",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Test-22-4-2020-NotHighlighted.aspx",
"uniqueId": "{591442E1-B64D-46C9-81CF-4672C63A5D8C}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "13",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-04-22T07:23:27Z"
}
}
},
{
"hitId": "EB99702E-59C0-45E1-919E-F87E407A888C",
"rank": 10,
"summary": "Hello! This is a Text web part in one of two columns in this section . You can click inside this text block when in Edit mode to make changes. Next to this paragraph is a column<ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test Post in teams",
"description": "Hello! This is a Text web part in one of two columns in this section . You can click inside this text block when in Edit mode to make changes. Next to this paragraph is a column that contains an image web part. Click the image, and you can use the t…",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/images/sitepagethumbnail.png",
"path": "https://test.sharepoint.com/sites/test/SitePages/Test-Post.aspx",
"uniqueId": "{EB99702E-59C0-45E1-919E-F87E407A888C}",
"spWebUrl": "https://test.sharepoint.com/sites/test",
"listItemID": "2",
"listID": "57d636f3-6d70-4ba3-8b47-f7775e401972",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-03-30T13:43:21Z"
}
}
},
{
"hitId": "AD6384BD-C232-4FCD-AE4E-BC0F61F91B77",
"rank": 11,
"summary": "",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test-News-22-04-2020",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/getpreview.ashx?guidSite=6939921445424f93b658328a712a834f&guidWeb=f5ad2fe7d9f14b968dce884fa6cc49cd&guidFile=cd7e4c54b5c54fc3b285a5e5135e5017&ext=png&ow=1110&oh=200",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Test-News-22-04-2020.aspx",
"uniqueId": "{AD6384BD-C232-4FCD-AE4E-BC0F61F91B77}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "12",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-04-22T07:22:08Z"
}
}
},
{
"hitId": "FB9CDB0F-7422-46C8-9499-BFE6DFE3BF93",
"rank": 12,
"summary": "",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test-Highlighted2",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/getpreview.ashx?guidSite=6939921445424f93b658328a712a834f&guidWeb=f5ad2fe7d9f14b968dce884fa6cc49cd&guidFile=ac954ab7f92849d8900e00cd5c0e8385&ext=jpeg&ow=2560&oh=1884",
"path": "https://test.sharepoint.com/sites/tea-point/SitePages/news-Test-Highlighted2.aspx",
"uniqueId": "{FB9CDB0F-7422-46C8-9499-BFE6DFE3BF93}",
"spWebUrl": "https://test.sharepoint.com/sites/tea-point",
"listItemID": "15",
"listID": "59930aeb-e78b-4bb4-a78c-5b7485d1445d",
"author": "Test User",
"firstPublishedDateOWSDATE": "2020-04-22T07:32:16Z"
}
}
},
{
"hitId": "C4F91266-AC1C-46F9-AE04-22E41E1584C8",
"rank": 13,
"summary": "How do you get started? Select 'Edit' to start working with this basic two-column template with an emphasis on text and examples of text formatting. With your page in edit mode, <ddd/>",
"resource": {
"@odata.type": "#microsoft.graph.listItem",
"fields": {
"title": "Test news in Test Team!",
"description": "How do you get started? Select 'Edit' to start working with this basic two-column template with an emphasis on text and examples of text formatting. With your page in edit mode, select this paragraph and replace it with your own text. Then, select t…",
"pictureThumbnailURL": "https://test.sharepoint.com/_layouts/15/images/sitepagethumbnail.png",
"path": "https://test.sharepoint.com/sites/Testteam646/SitePages/Test-news-in-Test-Team!.aspx",
"uniqueId": "{C4F91266-AC1C-46F9-AE04-22E41E1584C8}",
"spWebUrl": "https://test.sharepoint.com/sites/Testteam646",
"listItemID": "3",
"listID": "cfb20819-517b-4f63-96b9-6fd954f22933",
"author": "Test User",
"firstPublishedDateOWSDATE": "2021-04-01T06:31:49Z"
}
}
}
],
"total": 26,
"moreResultsAvailable": false
}
]
}
],
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.searchResponse)"
})
const newsProvider = new NewsProvider(graphClient);
const news = await newsProvider.getData();
expect(news.length).toBeGreaterThan(0);
expect(news[0].title).toBe("Test Post");
});
});

View File

@ -0,0 +1,89 @@
///<reference types="jest" />
import { PostInTeamsAction } from "../../../../src/manager/viewManager/viewActions/PostInTeamsAction";
describe("PostInTeamsAction", () => {
test("should load teams to state", async () => {
let mockTeams = [{
id: "1",
displayName: "team1"
}, {
id: "2",
displayName: "team2"
}, {
id: "3",
displayName: "team3"
}];
let mockManager = {
getJoinedTeams: () => Promise.resolve(mockTeams)
}
let actionHandler = new PostInTeamsAction(mockManager as any);
let quickView = {
setState: jest.fn(),
state: {}
}
let action = {
id: "loadTeams"
}
let spy = jest.spyOn(quickView, "setState");
await actionHandler.handleAction(action, quickView as any);
expect(spy).toBeCalledWith({ joinedTeams: mockTeams, showTeams: true });
});
test("should load channels to state", async () => {
let mockChannels = [{
id: "1",
displayName: "team1"
}, {
id: "2",
displayName: "team2"
}, {
id: "3",
displayName: "team3"
}];
let mockManager = {
getChannels: () => Promise.resolve(mockChannels)
}
let actionHandler = new PostInTeamsAction(mockManager as any);
let quickView = {
setState: jest.fn(),
state: {}
}
let action = {
id: "showSelectChannel",
data:{
selectTeamsDD: "1"
}
}
let spy = jest.spyOn(quickView, "setState");
await actionHandler.handleAction(action, quickView as any);
expect(spy).toBeCalledWith({ selectedTeamChannels: mockChannels, showChannels: true, selectedTeamId: "1" });
});
test("should post to selected channel", async () => {
let mockManager = {
shareNews: () => Promise.resolve()
}
let actionHandler = new PostInTeamsAction(mockManager as any);
let newsToShare = {
id: "test-news-1",
};
let quickView = {
setState: jest.fn(),
state: {
selectedTeamId: "1",
selectedNewsIndex: 0,
news:[newsToShare]
}
}
let action = {
id: "shareInSelectedChannel",
data:{
selectChannelDD: "2"
}
}
let spy = jest.spyOn(mockManager, "shareNews");
await actionHandler.handleAction(action, quickView as any);
expect(spy).toBeCalledWith(newsToShare, "1", "2");
});
});

View File

@ -0,0 +1,34 @@
import { IHttpClient, IHttpClientResponse } from "../../src/dal/http/IHttpClient";
export class MockGraphClient implements IHttpClient{
public responses: Map<string,any> = new Map<string,any>();
get(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.returnMock(url);
}
private returnMock(url: string) {
let responseBody = this.responses.get(url);
let response = {
ok: true,
statusText: "OK",
status: 200,
json: () => Promise.resolve(responseBody),
text: () => Promise.resolve(JSON.stringify(responseBody)),
blob: () => Promise.resolve(new Blob([JSON.stringify(responseBody)], { type: "application/json" }))
};
return Promise.resolve(response);
}
post(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
return this.returnMock(url);
}
patch(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
throw new Error("Method not implemented.");
}
put(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
throw new Error("Method not implemented.");
}
delete(url: string): Promise<IHttpClientResponse> {
throw new Error("Method not implemented.");
}
}

View File

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

View File

@ -0,0 +1,29 @@
{
"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-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}