Merge pull request #1 from SharePoint/master

Merge Master
This commit is contained in:
João Mendes 2020-04-15 20:12:25 +01:00 committed by GitHub
commit c9df896d86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 32399 additions and 41 deletions

View File

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

32
samples/react-global-news/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,13 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.10.0",
"libraryName": "react-global-news",
"libraryId": "ee849c9a-efd9-4257-99d0-ad0b1c0fb095",
"environment": "spo",
"packageManager": "npm",
"framework": "react",
"isCreatingSolution": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,84 @@
---
page_type: sample
products:
- office-sp
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
- Microsoft Graph
platforms:
- React
createdDate: 4/14/2020 12:00:00 AM
---
# React Global News
## Summary
This web part show a world news from various sources, it uses the API available on https://newsapi.org this collect information from news and blog sites arround the world.
Please go to https://newsapi.org to get more information.
![picture of the web part in action](assets/globalNews.gif)
![picture of the web part in action](assets/GlobalNews.png)
## Used SharePoint Framework Version
![1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
> none
## Solution
Solution|Author(s)
--------|---------
react-global-news | João Mendes
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 14, 2020|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
> Include any additional steps as needed.
## Features
Description of the web part with possible additional details than in short summary.
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Office-ui-fabric-react components
* Office-ui-fabric themes
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-global-news" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-global-news-client-side-solution",
"id": "ee849c9a-efd9-4257-99d0-ad0b1c0fb095",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-global-news.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 -->"
}

54
samples/react-global-news/gulpfile.js vendored Normal file
View File

@ -0,0 +1,54 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
// Create clean development package
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Webpack Bundle Anlayzer
* Reference and gulp task
*/
const bundleAnalyzer = require('webpack-bundle-analyzer');
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
const lastDirName = path.basename(__dirname);
const dropPath = path.join(__dirname, 'temp', 'stats');
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
generateStatsFile: true,
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
logLevel: 'error'
}));
return generatedConfiguration;
}
});
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);

18144
samples/react-global-news/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
{
"name": "react-global-news",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@material-ui/core": "^4.9.10",
"@material-ui/lab": "^4.0.0-alpha.49",
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/spfx-controls-react": "^1.17.0",
"@pnp/spfx-property-controls": "^1.17.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"gulp-sequence": "^1.0.0",
"jquery": "^3.5.0",
"lodash": "^4.17.15",
"material-ui": "^0.20.2",
"moment": "^2.24.0",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5",
"spfx-uifabric-themes": "^0.8.0",
"webpack-bundle-analyzer": "^3.7.0"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#0b0b0b",
"neutralLighter": "#151515",
"neutralLight": "#252525",
"neutralQuaternaryAlt": "#2f2f2f",
"neutralQuaternary": "#373737",
"neutralTertiaryAlt": "#595959",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#000000"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#2d2c2c",
"neutralLighter": "#2c2b2b",
"neutralLight": "#2a2929",
"neutralQuaternaryAlt": "#272626",
"neutralQuaternary": "#252525",
"neutralTertiaryAlt": "#242323",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#2d2c2c"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#ecebe9",
"neutralLighter": "#e8e7e6",
"neutralLight": "#dedddc",
"neutralQuaternaryAlt": "#cfcecd",
"neutralQuaternary": "#c6c5c4",
"neutralTertiaryAlt": "#bebdbc",
"neutralTertiary": "#b5b4b2",
"neutralSecondary": "#9d9c9a",
"neutralPrimaryAlt": "#868482",
"neutralPrimary": "#252423",
"neutralDark": "#565453",
"black": "#3e3d3b",
"white": "#f3f2f1"
}

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,13 @@
export interface IArticle {
source: {
id: string;
name: string;
};
author: string;
title: string;
description: string;
url: string;
urlToImage: string;
publishedAt: string;
content: string;
}

View File

@ -0,0 +1,8 @@
import { IArticle } from "./IArticle";
export interface INewsResults {
status: string;
totalResults: number;
articles: IArticle[];
}

View File

@ -0,0 +1,5 @@
export interface INewsResultsError {
status: string;
code: string;
message: string;
}

View File

@ -0,0 +1,11 @@
export interface ISource {
id: string;
name: string;
description: string;
url: string;
category: string;
language: string;
country: string;
}

View File

@ -0,0 +1,6 @@
import { ISource } from "./ISource";
export interface ISourcesResults {
status: string;
sources: ISource[];
}

View File

@ -0,0 +1,127 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import {
HttpClient,
HttpClientResponse,
IHttpClientOptions
} from "@microsoft/sp-http";
import { INewsResults } from "./INewsResults";
import { ISourcesResults } from "./ISourcesResults";
export default class dataservices {
private static _httpClient: HttpClient;
/*
initialize the static class
*/
public static async init(context: WebPartContext) {
//obtain the httpClient from the webpart context
this._httpClient = context.httpClient;
}
// Get Sources
public static async getSources(apiKey:string):Promise<ISourcesResults>{
let requestHeaders = new Headers();
let sourceUrl = `https://newsapi.org/v2/sources`;
let _corsUrl = `https://cors-anywhere.herokuapp.com/${sourceUrl}`;
if (!sourceUrl || !apiKey) return;
try {
let _url = new URL(sourceUrl);
let _count = 0;
_url.searchParams.forEach((v,k,p) =>{
_count ++;
});
if (_count !== 0 ) { // test if has parameters
_corsUrl = _corsUrl + `&apiKey=${apiKey}`; // has parameters addpikey to last one
} else {
_corsUrl = _corsUrl.replace('?','') + `?apiKey=${apiKey}`; // add parameter apikey
}
} catch (error) {
return; // not valid Url
}
requestHeaders.append("Accept", "application/json");
requestHeaders.append("content-type", "application/json");
try {
//set up get options
const requestGetOptions: IHttpClientOptions = {
method: "GET",
headers: requestHeaders,
mode: "cors"
};
let query: HttpClientResponse = await this._httpClient.fetch(
_corsUrl,
HttpClient.configurations.v1,
requestGetOptions
);
return await query.json();
} catch (error) {
console.log(error);
throw new Error('Is not possible to read news at this moment, please try later.');
}
}
// Get News
public static async getNews(
newsUrl: string,
apiKey: string,
page?:number,
): Promise<INewsResults> {
let requestHeaders = new Headers();
const defaultPageSize = 12;
let _page:number = page ? page : 1;
let _corsUrl = `https://cors-anywhere.herokuapp.com/${newsUrl}&page=${_page}`;
if (!newsUrl || !apiKey) return;
try {
let _url = new URL(newsUrl);
let _count = 0;
_url.searchParams.forEach((v,k,p) =>{
_count ++;
});
if (_count !== 0 ) { // test if has parameters
_corsUrl = _corsUrl + `&apiKey=${apiKey}`; // has parameters addpikey to last one
} else {
_corsUrl = _corsUrl.replace('?','') + `?apiKey=${apiKey}`; // add parameter apikey
}
// teste if has pagesize parameter must be equal to default parameter value of web part
if ( !_url.searchParams.has('pagesize')){
_corsUrl = _corsUrl + `&pageSize=${defaultPageSize}`; //
}
} catch (error) {
return; // not valid Url
}
requestHeaders.append("Accept", "application/json");
requestHeaders.append("content-type", "application/json");
try {
//set up get options
const requestGetOptions: IHttpClientOptions = {
method: "GET",
headers: requestHeaders,
mode: "cors"
};
let query: HttpClientResponse = await this._httpClient.fetch(
_corsUrl,
HttpClient.configurations.v1,
requestGetOptions
);
return await query.json();
} catch (error) {
console.log(error);
throw new Error('Is not possible to read news at this moment, please try later.');
}
}
}

View File

@ -0,0 +1,15 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from "@microsoft/sp-core-library";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface INewsProps {
title: string;
newsUrl: string;
apiKey: string;
context: WebPartContext;
updateProperty: (value: string) => void;
displayMode: DisplayMode;
viewOption:string;
pageSize: number;
themeVariant: IReadonlyTheme | undefined;
}

View File

@ -0,0 +1,10 @@
import { IArticle } from "../../services/IArticle";
export interface INewsState{
hasError:boolean;
articles: IArticle[];
errorMesage:string;
isLoading: boolean;
currentPage:number;
totalPages:number;
}

View File

@ -0,0 +1,370 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "./node_modules/spfx-uifabric-themes/office.theme";
@import "~office-ui-fabric-react/dist/sass/semanticSlots";
@media screen and (min-width: 320px) and (max-width: 1024px) {
.card {
flex-direction: column;
}
.image {
width: 100%;
height: auto;
}
.newsTitle {
margin-top: 20px;
}
}
/* mixin for multiline */
@mixin multiLineEllipsis($lineHeight: 1.5em, $lineCount: 1, $bgColor: white) {
overflow: hidden;
position: relative;
line-height: $lineHeight;
max-height: $lineHeight * $lineCount;
text-align: start;
/* @include ms-fontWeight-semibold; */
color: $ms-color-neutralSecondaryAlt;
@include ms-fontSize-m;
&:before {
content: "...";
position: absolute;
right: 0;
bottom: 0;
}
&:after {
content: "";
position: absolute;
right: 0;
width: 1em;
height: 1em;
margin-top: 0.2em;
/* background: $bgColor;*/
}
}
.News {
/*:global { */
padding: 10px;
.cardsTiles {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, auto));
column-gap: 1rem;
row-gap: 1.5rem;
grid-auto-rows: auto;
}
.cardsItem {
width: 100%;
margin-bottom: 20px;
display: grid;
grid-template-columns: auto;
grid-template-rows: auto;
}
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin-right: 15px;
margin-left: 15px;
margin-top: 10px;
margin-bottom: 7px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
.separator {
border-style: solid;
border-width: 1px;
border-color: #edebe9;
width: 100%;
margin-bottom: 10px;
}
}
.image {
object-fit: cover;
}
.newsTitle {
padding-top: 10px;
overflow: hidden;
margin-left: 16px;
margin-right: 16px;
text-overflow: ellipsis;
max-width: 100%;
@include ms-fontWeight-semibold;
text-align: start;
text-transform: uppercase;
}
.secondaryText {
position: relative;
display: block;
overflow: hidden;
padding-top: 10px;
margin-left: 16px;
margin-right: 16px;
height: 55px;
}
.container {
display: flex;
flex-direction: column;
margin-top: 15px;
max-width: 100%;
}
[data-theme="default"] {
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin: 5px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
}
[data-theme="dark"] {
.nonewsIcon {
color: $ms-color-white;
}
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin: 5px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralSecondaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
.secondaryText {
position: relative;
display: block;
overflow: hidden;
padding-top: 10px;
color: $ms-color-neutralTertiaryAlt;
margin-left: 16px;
margin-right: 16px;
height: 55px;
}
.author {
margin-top: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
}
[data-theme="contrast"] {
.nonewsIcon {
color: $ms-color-white;
}
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin: 5px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
.author {
margin-top: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
}
.author {
margin-top: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.date {
@include ms-font-s-plus;
max-width: 200px;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 15px;
}
.nonewsIcon {
font-size: 96px;
/* color: $ms-color-neutralPrimary;*/
width: 96px;
height: 96px;
}
.nonewsMessage {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-top: 0px;
/* color: $ms-color-neutralPrimary;*/
max-width: 500px;
@include ms-fontWeight-semibold;
}
.title {
margin-left: 15px;
}
/*}*/
:global {
// default theme
$default-background: #f3f2f1;
$default-color: #252423;
$default-button-background: #6264a7;
$default-Button-color: #f3f2f1;
// dark theme
$dark-background: #2d2c2c;
$dark-color: #ffffff;
$dark-button-background: #6264a7;
$dark-button-color: #2d2c2c;
// contrast theme
$contrast-background: #000000;
$contrast-color: #ffffff;
$contrast-button-background: #b5c01c;
$contrast-Button-color: #000000;
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $ms-color-neutralPrimary !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $ms-color-neutralPrimary !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $ms-color-white !important;
background-color: $ms-themePrimary !important;
}
.MuiPaginationItem-page:hover {
background-color: $ms-themePrimary !important;
}
[data-theme="dark"] {
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $dark-color !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $dark-color !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $dark-color !important;
background-color: $dark-button-background !important;
}
.MuiPaginationItem-page:hover {
background-color: $dark-button-background !important;
}
.ms-Dropdown-optionText {
color: $ms-color-white !important;
}
[class^="propertyPaneFooter_"] {
background-color: $dark-background;
}
}
[data-theme="default"] {
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $default-color !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $default-color !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $ms-color-white !important;
background-color: $default-button-background !important;
}
.MuiPaginationItem-page:hover {
background-color: $default-button-background !important;
}
[class^="propertyPaneFooter_"] {
background-color: $default-background;
}
}
[data-theme="contrast"] {
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $contrast-color !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $contrast-color !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $contrast-color !important;
background-color: $contrast-button-background !important;
}
.MuiPaginationItem-page:hover {
background-color: $contrast-button-background !important;
}
.ms-Dropdown-optionText {
color: $ms-color-white !important;
}
[class^="propertyPaneFooter_"] {
background-color: $contrast-background;
}
}
}

View File

@ -0,0 +1,197 @@
import * as React from "react";
import styles from "./News.module.scss";
import { INewsProps } from "./INewsProps";
import { INewsState } from "./INewsState";
import { escape } from "@microsoft/sp-lodash-subset";
import dataservices from "../../services/dataservices";
import { NewsItem } from "./NewsItem/NewsItem";
import { NoNews } from "./NoNews";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { NewsTile } from "./NewsTile/NewsTile";
import * as strings from "NewsWebPartStrings";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import {
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
mergeStyles,
getTheme
} from "office-ui-fabric-react";
import { INewsResults } from "../../services/INewsResults";
import { IArticle } from "../../services/IArticle";
import Pagination from "@material-ui/lab/Pagination";
import { Customizer } from "@uifabric/utilities/lib/";
const theme = getTheme();
export default class News extends React.Component<INewsProps, INewsState> {
private _totalResuts: number = 0;
constructor(props: INewsProps) {
super(props);
this.state = {
isLoading: false,
hasError: false,
errorMesage: null,
articles: [],
currentPage: 1,
totalPages: 0
};
}
private _onConfigure = () => {
this.props.context.propertyPane.open();
}
// Component Did Mount
public async componentDidMount(): Promise<void> {
//await dataservices.init(this.props.context);
this._getNews(
this.props.newsUrl,
this.props.apiKey,
this.state.currentPage,
);
}
// Component Did Update
public async componentDidUpdate(
prevProps: INewsProps,
prevState: INewsState
): Promise<void> {
if (
this.props.newsUrl !== prevProps.newsUrl ||
this.props.apiKey !== prevProps.apiKey
) {
this._getNews(
this.props.newsUrl,
this.props.apiKey,
1 // force current page to 1 because new propeerties defined
);
}
}
// Get News from newsApi.org
private _getNews = async (newsUrl: string, apiKey: string, page?: number) => {
try {
const { pageSize } = this.props;
this.setState({ isLoading: true, hasError: false, errorMesage: "" });
const results: any = await dataservices.getNews(newsUrl, apiKey, page);
if (results && results.status == "error") {
throw new Error(results.message);
}
// calculate number of pages
let _reminder: number = (results as INewsResults).totalResults % pageSize; // get Reminder
_reminder = _reminder ? 1 : 0;
const _totalPages: number =
parseInt((results.totalResults / pageSize).toString()) + _reminder;
this.setState({
articles: results ? results.articles : [],
isLoading: false,
hasError: false,
errorMesage: "",
totalPages: _totalPages,
currentPage: page
});
} catch (error) {
console.log("error", error);
this.setState({
isLoading: false,
hasError: true,
errorMesage: error.message
});
}
}
// Render WebPart
public render(): React.ReactElement<INewsProps> {
const { isLoading, errorMesage, articles, hasError } = this.state;
let _renderNews: JSX.Element[] = [];
if (articles && articles.length > 0) {
for (const article of articles) {
if (!this.props.viewOption || this.props.viewOption == "list") {
_renderNews.push(<NewsItem article={article} key={article.title} />);
} else {
_renderNews.push(<NewsTile article={article} key={article.title} />);
}
}
}
return (
<Customizer settings={{ theme: this.props.themeVariant }}>
<div className={styles.News}>
{!this.props.apiKey || !this.props.newsUrl ? (
<Placeholder
iconName="Edit"
iconText={strings.ConfigureWebPartMessage}
description={strings.configureWebPartTextMessage}
buttonLabel={strings.ConfigureWebPartButtonLabel}
onConfigure={this._onConfigure}
/>
) : (
<>
<WebPartTitle
displayMode={this.props.displayMode}
title={this.props.title}
className={styles.title}
updateProperty={this.props.updateProperty}
/>
{isLoading ? (
<Spinner size={SpinnerSize.medium} label="Loading..." />
) : hasError ? (
<>
<MessageBar messageBarType={MessageBarType.error}>
{errorMesage}
</MessageBar>
</>
) : _renderNews.length > 0 ? (
<>
<div
className={
this.props.viewOption == "tiles"
? styles.cardsTiles
: styles.cardsItem
}
>
{_renderNews}
</div>
<div
style={{
display: "flex",
marginTop: 10,
marginBottom: 20,
justifyContent: "center"
}}
>
{this.state.totalPages > 1 && (
<>
<Pagination
color="primary"
count={this.state.totalPages}
page={this.state.currentPage}
onChange={(event, page) => {
this._getNews(
this.props.newsUrl,
this.props.apiKey,
page
);
}}
/>
</>
)}
</div>
</>
) : (
<NoNews />
)}
</>
)}
</div>
</Customizer>
);
}
}

View File

@ -0,0 +1,95 @@
import { IArticle } from "../../../services/IArticle";
import {
Label,
Text,
DocumentCardActivity
} from "office-ui-fabric-react";
import * as moment from "moment";
import styles from "../News.module.scss";
import * as React from "react";
import * as strings from "NewsWebPartStrings";
const _defaultImage:string = require("./../../../../assets/news.jpg");
export const NewsItem = (props: { article: IArticle }) => {
let { article } = props;
return (
<>
<div
className={styles.card}
onClick={ev => {
ev.preventDefault();
window.open(article.url, "_blank");
}}
>
<div
style={{
width:180,
height: '100%'
}}
>
<img
className={styles.image}
src={
article.urlToImage
? article.urlToImage
: _defaultImage
}
width="180"
height="180"
onError={(ev)=> {
ev.currentTarget.setAttribute('src',_defaultImage); // set Default image if can not load article image
}}
></img>
</div>
<div
style={{
width: '100%',
display:'Flex',
height: '100%',
flexDirection: 'column',
justifyContent: 'sflex-start'
}}
>
<Label className={styles.newsTitle} >
{article.title.trim()}
</Label>
<div>
<Text
className={styles.secondaryText}
variant='medium'
block
title={
article.description
? article.description.trim()
: strings.CanNotShowArticleTextMessage
}
>
{article.description
? article.description.trim()
: strings.CanNotShowArticleTextMessage}
</Text>
<DocumentCardActivity
className={styles.author}
activity={moment(article.publishedAt).format("LL")}
people={[
{
name: article.source.name
? article.source.name
: article.author
? article.author
: strings.AuthorNotAvailableMessage,
profileImageSrc: ""
}
]}
/>
</div>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,178 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@media only screen and (min-device-width: 320px) and (max-device-width: 768px) {
.card {
flex-direction: column;
}
.image {
width: 100%;
}
.newsTitle {
margin-top: 20px;
}
}
@media screen and (min-width: 320px) and (max-width: 1024px){
}
.image {
object-fit: cover;
}
.newsTitle {
font-size: 20px;
padding-top: 0px;
/* color: $ms-color-themePrimary; */
margin-bottom: 20px;
@include ms-fontWeight-semibold;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.description {
/* color: $ms-color-neutralSecondaryAlt; */
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
margin-bottom: 10px;
}
.author {
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.date {
@include ms-font-s-plus;
max-width: 200px;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.descriptionTile {
/* color: $ms-color-neutralSecondaryAlt;*/
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
max-height: 135px;
min-height: 135px;
margin-bottom: 15px;
display: block;
overflow: hidden;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.separator {
border-top: 1px solid lighten(#333, 70%);
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
.cardTile {
max-width: 100%
}
[data-theme="default"] {
.cardTile {
border-color: $ms-color-neutralTertiaryAlt;
}
.separator {
border-top: 1px solid $ms-color-neutralTertiaryAlt;
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
}
[data-theme="dark"] {
.cardTile {
border-color: $ms-color-neutralSecondaryAlt;
}
.separator {
border-top: 1px solid $ms-color-neutralSecondaryAlt;
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
.cardTile:hover {
border-color: $ms-color-neutralTertiary;
}
.descriptionTile {
/* color: $ms-color-neutralSecondaryAlt;*/
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
max-height: 135px;
min-height: 135px;
margin-bottom: 15px;
display: block;
overflow: hidden;
color: $ms-color-neutralTertiaryAlt;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
:global {
.ms-Persona-initials {
color: $ms-color-white;
}
}
}
[data-theme="contrast"] {
.cardTile {
border-color: $ms-color-neutralTertiaryAlt;
}
.separator {
border-top: 1px solid $ms-color-neutralTertiaryAlt;
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
.descriptionTile {
/* color: $ms-color-neutralSecondaryAlt;*/
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
max-height: 135px;
min-height: 135px;
margin-bottom: 15px;
display: block;
overflow: hidden;
color: $ms-color-neutralTertiaryAlt;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
:global {
.ms-Persona-initials {
color: $ms-color-white;
}
}
}

View File

@ -0,0 +1,84 @@
import { IArticle } from "../../../services/IArticle";
import {
Text,
DocumentCard,
DocumentCardTitle,
DocumentCardDetails,
Persona,
DocumentCardActivity,
} from "office-ui-fabric-react";
import * as moment from "moment";
import styles from "./NewsTile.module.scss";
import * as React from "react";
import * as strings from "NewsWebPartStrings";
import { escape } from "@microsoft/sp-lodash-subset";
const _defaultImage: string = require("../../../../assets/news.jpg");
export const NewsTile = (props: { article: IArticle }) => {
let { article } = props;
return (
<>
<DocumentCard
className={styles.cardTile}
onClick={ev => {
ev.preventDefault();
window.open(article.url, "_blank");
}}
>
<div style={{ minWidth: "100%", maxHeight: 160 }}>
<img
className={styles.image}
src={article.urlToImage ? article.urlToImage : _defaultImage}
width={"100%"}
height={160}
onError={ev => {
ev.currentTarget.setAttribute("src", _defaultImage); // set Default image if can not load article image
}}
></img>
</div>
<div
style={{ display: "flex", flexDirection: "column", marginTop: 25 }}
>
<DocumentCardTitle
title={article.title}
shouldTruncate={true}
className={styles.newsTitle}
></DocumentCardTitle>
<DocumentCardDetails styles={{ root: { paddingBottom: 25 } }}>
<Text block variant="mediumPlus" className={styles.descriptionTile}>
{article.description
? article.description
: strings.CanNotShowArticleTextMessage}
</Text>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center"
}}
></div>
</DocumentCardDetails>
<div className={styles.separator}></div>
<DocumentCardActivity
className={styles.author}
activity={moment(article.publishedAt).format("LL")}
people={[
{
name: article.source.name
? article.source.name
: article.author
? article.author
: strings.AuthorNotAvailableMessage,
profileImageSrc: "",
}
] }
/>
</div>
</DocumentCard>
</>
);
};

View File

@ -0,0 +1,31 @@
import {
Icon,
Stack,
DocumentCardTitle,
} from "office-ui-fabric-react";
import styles from "./News.module.scss";
import * as React from "react";
export const NoNews = () => {
return (
<>
<div className="card">
<Stack
style={{ justifyContent: "center", alignItems: "center" }}
verticalAlign="center"
tokens={{ childrenGap: 5 }}
>
<div>
<Icon iconName="News" className={styles.nonewsIcon} />
</div>
<DocumentCardTitle
title="No News find at this moment"
className={styles.nonewsMessage}
></DocumentCardTitle>
</Stack>
</div>
<div className="separator"></div>
</>
);
};

View File

@ -0,0 +1,37 @@
import { IArticle } from "../../../services/IArticle";
import {
Icon,
Label,
Text,
Stack,
Image,
ImageFit,
DocumentCard,
DocumentCardTitle,
DocumentCardDetails
} from "office-ui-fabric-react";
import moment from "moment";
import styles from "../News.module.scss";
import React from "react";
export const NoNews = () => {
return (
<>
<div
className="card"
>
<Stack style={{justifyContent: 'center', alignItems:'center'}} verticalAlign="center" tokens={{ childrenGap: 5 }}>
<div>
<Icon iconName="News" className={styles.nonewsIcon}/>
</div>
<DocumentCardTitle
title="No News find at this moment"
className={styles.nonewsMessage}
></DocumentCardTitle>
</Stack>
</div>
<div className="separator"></div>
</>
);
};

View File

@ -0,0 +1,39 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "137868a8-8e7e-4e06-85f3-a8913f5ff71c",
"alias": "NewsWebPart",
"componentType": "WebPart",
"supportsFullBleed": true, // suport to full width sections
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
"supportsThemeVariants": true,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart","TeamsTab","TeamsPersonalApp","SharePointFullPage"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "World News" },
"description": { "default": "World News WebPart" },
"officeFabricIconFontName": "NewsSearch",
"properties": {
"title": "News",
"newsUrl": "https://newsapi.org/v2/everything?q=*",
"apiKey": "3f28c6d88b804e75a66840dbb5aa84b1",
"endpoint":"",
"queryTitleOnly" : "",
"domains": "",
"excludeDomains": "",
"language":"",
"country": "ALL",
"category": "general",
"pagesize": 12,
"viewOption": "tiles" // tiles or list
}
}]
}

View File

@ -0,0 +1,541 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneChoiceGroup,
PropertyPaneToggle,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
PropertyPaneSlider,
PropertyPaneHorizontalRule,
PropertyPaneLabel
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import * as lodash from "lodash";
import * as strings from "NewsWebPartStrings";
import News from "../components/News";
import { INewsProps } from "../components/INewsProps";
import dataservices from "../../services/dataservices";
import { DisplayMode } from "@microsoft/sp-core-library";
import * as $ from "jquery";
import { loadTheme, IDropdownOption } from "office-ui-fabric-react";
import { PropertyFieldMultiSelect } from "@pnp/spfx-property-controls/lib/PropertyFieldMultiSelect";
import { ISourcesResults } from "../../services/ISourcesResults";
import { ISource } from "../../services/ISource";
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
const languages: IPropertyPaneDropdownOption[] = [
{ key: "all", text: "All Languages" },
{ key: "ar", text: "Arabic" },
{ key: "de", text: "German" },
{ key: "en", text: "English" },
{ key: "es", text: "Castilian" },
{ key: "fr", text: "French" },
{ key: "he", text: "Hebrew" },
{ key: "it", text: "Italian" },
{ key: "nl", text: "Dutch" },
{ key: "no", text: "Norwegian" },
{ key: "pt", text: "Portuguese" },
{ key: "ru", text: "Russian" },
{ key: "se", text: "Northern Sami" },
{ key: "zh", text: "Chinese" }
];
const countries: IPropertyPaneDropdownOption[] = [
{ key: "ALL", text: "All Countries" },
{ key: "AE", text: "United Arab Emirates" },
{ key: "AR", text: "Argentina" },
{ key: "AT", text: "Austria" },
{ key: "AU", text: "Australia" },
{ key: "BE", text: "Belgium" },
{ key: "BG", text: "Bulgaria" },
{ key: "BR", text: "Brazil" },
{ key: "CA", text: "Canada" },
{ key: "CH", text: "Switzerland" },
{ key: "CN", text: "China" },
{ key: "CO", text: "Colombia" },
{ key: "CU", text: "Cuba" },
{ key: "CZ", text: "Czech Republic" },
{ key: "DE", text: "Germany" },
{ key: "EG", text: "Egypt" },
{ key: "FR", text: "France" },
{ key: "GB", text: "United Kingdom" },
{ key: "GR", text: "Greece" },
{ key: "HK", text: "Hong Kong" },
{ key: "HU", text: "Hungary" },
{ key: "ID", text: "Indonesia" },
{ key: "IE", text: "Ireland" },
{ key: "IL", text: "Israel" },
{ key: "IN", text: "India" },
{ key: "IT", text: "Italy" },
{ key: "JP", text: "Japan" },
{ key: "KR", text: "Korea, Republic of" },
{ key: "LT", text: "Lithuania" },
{ key: "LV", text: "Latvia" },
{ key: "MA", text: "Morocco" },
{ key: "MX", text: "Mexico" },
{ key: "MY", text: "Malaysia" },
{ key: "NG", text: "Nigeria" },
{ key: "NL", text: "Netherlands" },
{ key: "NO", text: "Norway" },
{ key: "NZ", text: "New Zealand" },
{ key: "PH", text: "Philippines" },
{ key: "PL", text: "Poland" },
{ key: "PT", text: "Portugal" },
{ key: "RO", text: "Romania" },
{ key: "RS", text: "Serbia" },
{ key: "RU", text: "Russian Federation" },
{ key: "SA", text: "Saudi Arabia" },
{ key: "SE", text: "Sweden" },
{ key: "SG", text: "Singapore" },
{ key: "SI", text: "Slovenia" },
{ key: "SK", text: "Slovakia" },
{ key: "TH", text: "Thailand" },
{ key: "TR", text: "Turkey" },
{ key: "TW", text: "Taiwan, Province of China" },
{ key: "UA", text: "Ukraine" },
{ key: "US", text: "United States" },
{ key: "VE", text: "Venezuela, Bolivarian Republic of" },
{ key: "ZA", text: "South Africa" }
];
const categories: IPropertyPaneDropdownOption[] = [
{ key: "business", text: "business" },
{ key: "entertainment", text: "entertainment" },
{ key: "general", text: "general" },
{ key: "health", text: "health" },
{ key: "science", text: "science" },
{ key: "sports", text: "sports" },
{ key: "technology", text: "technology" }
];
const teamsDefaultTheme = require("../../common/TeamsDefaultTheme.json");
const teamsDarkTheme = require("../../common/TeamsDarkTheme.json");
const teamsContrastTheme = require("../../common/TeamsContrastTheme.json");
export interface INewsWebPartProps {
title: string;
newsUrl: string;
apiKey: string;
endpoint: number;
queryTitleOnly: boolean;
domains: string;
excludeDomains: string;
language: string;
country: string;
category: string;
query: string;
pagesize: number;
viewOption: string;
sources: string[];
}
export default class NewsWebPart extends BaseClientSideWebPart<
INewsWebPartProps
> {
private _sourcesOptions: IDropdownOption[] = [];
private _themeProvider: ThemeProvider;
private _themeVariant: IReadonlyTheme | undefined;
protected async onInit<T>(): Promise<T> {
await dataservices.init(this.context);
this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
// If it exists, get the theme variant
this._themeVariant = this._themeProvider.tryGetTheme();
// Register a handler to be notified if the theme variant changes
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
// test if is teams context
if (this.context.sdks.microsoftTeams) {
// in teams ?
const context = this.context.sdks.microsoftTeams!.context;
this._applyTheme(context.theme || "default");
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(
this._applyTheme
);
}
return Promise.resolve();
}
/**
* Update the current theme variant reference and re-render.
*
* @param args The new theme
*/
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
this._themeVariant = args.theme;
this.render();
}
// Apply Teams Context
private _applyTheme = (theme: string): void => {
this.context.domElement.setAttribute("data-theme", theme);
document.body.setAttribute("data-theme", theme);
if (theme == "dark") {
loadTheme({
palette: teamsDarkTheme
});
}
if (theme == "default") {
loadTheme({
palette: teamsDefaultTheme
});
}
if (theme == "contrast") {
loadTheme({
palette: teamsContrastTheme
});
}
}
private _validateNewsUrl = (value: string): string => {
try {
let _url = new URL(value);
return "";
} catch (error) {
return "news Url is not valid, please specify valid url";
}
}
public updateProperty = (value: string) => {
this.properties.title = value;
}
private _getSources = async (apiKey: string) => {
let _resultSources: ISourcesResults = await dataservices.getSources(apiKey);
try {
if (_resultSources && _resultSources.sources.length > 0) {
for (const source of _resultSources.sources) {
this._sourcesOptions.push({ key: source.id, text: source.name });
}
return;
}
} catch (error) {
console.log("Error loading Sources", error);
return;
}
}
// Render WebPart
public render(): void {
const element: React.ReactElement<INewsProps> = React.createElement(
News,
{
newsUrl: this.properties.newsUrl,
apiKey: this.properties.apiKey,
context: this.context,
title: this.properties.title,
updateProperty: this.updateProperty,
displayMode: this.displayMode,
viewOption: this.properties.viewOption,
pageSize: this.properties.pagesize,
themeVariant: this._themeVariant,
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected async onPropertyPaneConfigurationStart() {
await this._getSources(this.properties.apiKey);
this.context.propertyPane.refresh();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
let _showPropertyQueryTitle: any = "";
let _showPropertyDomains: any = "";
let _showPropertyexcludeDomains: any = "";
let _showPropertyLanguage: any = "";
let _showPropertyCountry: any = "";
let _showPropertyCategory: any = "";
let _viewOption: string = undefined;
if (!this.properties.viewOption || this.properties.viewOption === "list") {
_viewOption = "list";
} else {
_viewOption = "tiles";
}
if (!this.properties.newsUrl) {
this.properties.newsUrl =
"https://newsapi.org/v2/top-headlines?category=general&sortBy=publishedAt";
}
if (!this.properties.endpoint) {
this.properties.endpoint = 1;
}
if (!this.properties.category) {
this.properties.category = "general";
}
// teste if All News or TOP Headding news
switch (this.properties.endpoint) {
case 2:
this.properties.country = "";
this.properties.category = "";
_showPropertyCountry = "";
_showPropertyCategory = "";
_showPropertyQueryTitle = PropertyPaneToggle("queryTitleOnly", {
label: "Search on Article Title Only",
onText: "On",
offText: "Off",
checked: this.properties.queryTitleOnly
});
_showPropertyDomains = PropertyPaneTextField("domains", {
label: "Selected Domains",
value: this.properties.domains,
description:
"comma-seperated (eg bbc.co.uk, techcrunch.com, engadget.com)"
});
_showPropertyexcludeDomains = PropertyPaneTextField("excludeDomains", {
label: "Exclude Domains",
value: this.properties.excludeDomains,
description:
"comma-seperated (eg bbc.co.uk, techcrunch.com, engadget.com)"
});
// Create a news URL API
this.properties.newsUrl = `https://newsapi.org/v2/everything?sortBy=publishedAt`;
// Test Properties
// Query on Article Title
if (this.properties.queryTitleOnly) {
if (
this.properties.query &&
this.properties.query.trim().length > 0
) {
let _query: string = encodeURIComponent(this.properties.query);
this.properties.newsUrl = `${this.properties.newsUrl}&qInTitle=${_query}`;
} else {
this.properties.newsUrl = `${this.properties.newsUrl}&qInTitle=*`;
}
}
// Domains
if (this.properties.domains) {
this.properties.newsUrl = `${this.properties.newsUrl}&domains=${this.properties.domains}`;
}
// Excluded Domains
if (this.properties.excludeDomains) {
this.properties.newsUrl = `${this.properties.newsUrl}&excludeDomains=${this.properties.excludeDomains}`;
}
// Language
if (this.properties.language && this.properties.language !== "all") {
this.properties.newsUrl = `${this.properties.newsUrl}&language=${this.properties.language}`;
}
// Query Title Only
if (!this.properties.queryTitleOnly) {
if (
this.properties.query &&
this.properties.query.trim().length > 0
) {
let _query: string = encodeURIComponent(this.properties.query);
this.properties.newsUrl = `${this.properties.newsUrl}&q=${_query}`;
} else {
this.properties.newsUrl = `${this.properties.newsUrl}&q=*`;
}
}
if (this.properties.pagesize) {
this.properties.newsUrl = `${this.properties.newsUrl}&pageSize=${this.properties.pagesize}`;
}
if (this.properties.sources && this.properties.sources.length > 0) {
if (this.properties.sources.length < 20) {
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.join()}`;
} else {
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.slice(0, 19).join()}`;
}
}
_showPropertyLanguage = PropertyPaneDropdown("language", {
label: "Show Articles in this language",
options: lodash.sortBy(languages, ["key"]),
selectedKey: this.properties.language || "all"
});
break;
// Top Heading
case 1:
// Reset Properties Vars
this.properties.queryTitleOnly = false;
this.properties.domains = "";
this.properties.excludeDomains = "";
this.properties.language = "";
_showPropertyQueryTitle = "";
_showPropertyDomains = "";
_showPropertyexcludeDomains = "";
_showPropertyLanguage = "";
this.properties.newsUrl = `https://newsapi.org/v2/top-headlines?sortBy=publishedAt`;
if (this.properties.query && this.properties.query.trim().length > 0) {
let _query: string = encodeURIComponent(this.properties.query);
this.properties.newsUrl = `${this.properties.newsUrl}&q=${_query}`;
}
if (this.properties.pagesize) {
this.properties.newsUrl = `${this.properties.newsUrl}&pageSize=${this.properties.pagesize}`;
}
// Has sources ? add parameter to newsURl disable country and Category
if (this.properties.sources && this.properties.sources.length > 0) {
if (this.properties.sources.length < 20) {
// only the first 20 sources selectd limited by API
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.join()}`;
} else {
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.slice(0, 19).join()}`;
}
} else {
// Show Category and Country if sources is not specified
if (this.properties.category) {
this.properties.newsUrl = `${this.properties.newsUrl}&category=${this.properties.category}`;
}
if (this.properties.country && this.properties.country !== "ALL") {
this.properties.newsUrl = `${this.properties.newsUrl}&country=${this.properties.country}`;
}
_showPropertyCountry = PropertyPaneDropdown("country", {
label: "Country",
options: lodash.sortBy(countries, ["text"]),
selectedKey: this.properties.country || "ALL"
});
_showPropertyCategory = PropertyPaneDropdown("category", {
label: "Category",
options: lodash.sortBy(categories, ["key"]),
selectedKey: this.properties.category || "general"
});
}
break;
default:
break;
}
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("title", {
label: strings.Title,
value: this.properties.title
}),
PropertyPaneTextField("query", {
label: "Search Keyword or Phrase",
value: this.properties.query
}),
PropertyFieldMultiSelect("sources", {
key: "sources",
label: "Sources",
disabled: false,
options: this._sourcesOptions,
selectedKeys: this.properties.sources
}),
PropertyPaneChoiceGroup("endpoint", {
label: "Show Articles from:",
options: [
{ text: "Top Headlines", key: 1 },
{ text: "All News", key: 2 }
]
}),
_showPropertyCountry,
_showPropertyCategory,
_showPropertyQueryTitle,
_showPropertyDomains,
_showPropertyexcludeDomains,
_showPropertyLanguage
]
}
]
},
{
header: {
description: strings.ViewSettings
},
groups: [
{
groupFields: [
PropertyPaneChoiceGroup("viewOption", {
label: strings.ViewOption,
options: [
{
text: "List View",
key: "list",
checked: _viewOption === "list" ? true : false,
iconProps: { officeFabricIconFontName: "list" }
},
{
text: "Tiles View",
key: "tiles",
checked: _viewOption === "ltiles" ? true : false,
iconProps: { officeFabricIconFontName: "Tiles" }
}
]
}),
PropertyPaneLabel("", { text: "" }),
PropertyPaneSlider("pagesize", {
label: strings.PageSizeLabel,
max: 100,
min: 3,
step: 1,
showValue: true,
value: this.properties.pagesize
}),
PropertyPaneLabel("", { text: strings.APILabelText }),
PropertyPaneTextField("apiKey", {
label: strings.ApiKey,
value: this.properties.apiKey,
validateOnFocusOut: true,
onGetErrorMessage: value => {
if (!value) {
return "ApiKey is Required";
} else {
return "";
}
}
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,18 @@
define([], function() {
return {
ConfigureWebPartButtonLabel: "Configure",
configureWebPartTextMessage: "Please configure the web part.",
ConfigureWebPartMessage: "Configure News web part",
AuthorNotAvailableMessage: "Author not Available",
CanNotShowArticleTextMessage: "Can not show article text, please click to see the full article.",
APILabelText: "APIKEY use on API Calls, Please go to https://newsapi.org see more details",
PageSizeLabel: "Number of Articles per page",
ViewOption: "View Option",
ViewSettings: "View Settings",
Title: "Title",
"PropertyPaneDescription": "This Web Part Get News from newsapi.org API, please go to https://newsapi.org for more information.",
"BasicGroupName": "News",
"NewsUrlFieldLabel": "NewsApi Url",
"ApiKey": "APIKEY"
}
});

View File

@ -0,0 +1,21 @@
declare interface INewsWebPartStrings {
ConfigureWebPartButtonLabel: string;
configureWebPartTextMessage: string;
ConfigureWebPartMessage: string;
AuthorNotAvailableMessage: string;
CanNotShowArticleTextMessage: string;
APILabelText: string;
PageSizeLabel: string;
ViewOption: string;
ViewSettings: string;
Title: string;
PropertyPaneDescription: string;
BasicGroupName: string;
NewsUrlFieldLabel: string;
ApiKey: string;
}
declare module 'NewsWebPartStrings' {
const strings: INewsWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -121,7 +121,7 @@ export function ManageApps(appProps: IManageAppsProps) {
},
{
title: "Url",
field: "Url",
field: "url",
editComponent: props => (
<TextField
underlined
@ -279,6 +279,7 @@ export function ManageApps(appProps: IManageAppsProps) {
showTitle:false,
searchFieldAlignment:'left',
pageSize: 7,
pageSizeOptions: [],
search: true,
minBodyHeight: "100%"
}}

View File

@ -150,7 +150,7 @@ export default class PnPListView extends React.Component<IPnPListViewProps, IPnP
* @param item ListView item
*/
private _renderExperience(item?: any): any {
const experience: string = item['Expirience'];
const experience: string = item['Experience'];
return <FieldTextRenderer
text={experience}

View File

@ -15,7 +15,9 @@ extensions:
---
# Script editor web part for modern pages built in React
This version is built on SPFx v1.4.1 and also support SharePoint on-premises. If you want a version for SPO only go to [react-script-editor](../react-script-editor).
This version is built on SPFx v1.4.1. The version works for both SharePoint Online and for SharePoint on-premises.
The SPO only version can be found at [react-script-editor](../react-script-editor) and it is similar in functionality but has an improved editor experience.
## Summary
Coming from old classic SharePoint pages you might have existing script solutions you want to re-use on a modern page
@ -145,8 +147,8 @@ Version|Date|Comments
* gulp clean
* gulp bundle --ship
* gulp package-solution --ship
* Upload .sppkg file from sharepoint\solution to your tenant App Catalog
* E.g.: https://&lt;tenant&gt;.sharepoint.com/sites/AppCatalog/AppCatalog
* Upload .sppkg file from sharepoint\solution to your tenant App Catalog or to your on-premises app catalog.
* E.g.: https://&lt;tenant&gt;.sharepoint.com/sites/AppCatalog/AppCatalog or https://myserver/sites/apps
* Add the web part to a site collection, and test it on a page
### Deploy to non-script sites / modern team sites

View File

@ -1,15 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"script-editor-bundle": {
"components": [{
"entrypoint": "./lib/webparts/scriptEditor/ScriptEditorWebPart.js",
"manifest": "./src/webparts/scriptEditor/ScriptEditorWebPart.manifest.json"
}]
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"script-editor-bundle": {
"components": [
{
"entrypoint": "./lib/webparts/scriptEditor/ScriptEditorWebPart.js",
"manifest": "./src/webparts/scriptEditor/ScriptEditorWebPart.manifest.json"
}
},
"localizedResources": {
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
]
}
}
},
"localizedResources": {
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -9,4 +9,4 @@
"environment": "spo",
"componentType": "webpart"
}
}
}

View File

@ -21,19 +21,17 @@ React-securitygrid is an SPFX webpart that uses React and Office-UI-Fabric to re
![config panel](./src/images/MainDisplay.gif)
Empty libraries are displayed with a black folder icon, those with items are displayed with a white folder. The user can expand a list or library by clicking on the desired row. (If the library or folder has more than 5000 items an error will be displayed ) For deeply nested folders the Title column can be resized by drag and drop. The display shows the appropriate icon circle if the user has the selected permission to the given list, library, file or folder. (NOTE:The grid does not currently take into account access give via membership in an active directory group-- coming soon!).
Empty libraries are displayed withh a black folder icon, those with items are displayed with a white folder. The user can expand a list or library by clicking on the desired row. (If the library or folder has more than 5000 items an error will be displayed ) For deeply nested folders the Title column can be resized by drag and drop. The display shows a the appropriate icpn circle if the user has the selected permission to the given list, library, file or folder. (NOTE:The grid does not currently take into account access give via membership in an active directory group-- coming soon!). IMPORTANT: The user must have permissions to access lists and enumerate permissions in order to view the grid.
> IMPORTANT: The user must have permissions to access lists and enumerate permissions in order to view the grid.
The user can change the permission being tested by clicking the Permission in the command bar and selecting the new Permission:
The user can change the permission being tested by cliking the Permission in the command bar and selecting a new Permission:
![permission panel](./src/images/selectPermissionsPopout.PNG)
The user can change which users are being shown in the grid by selecting the users button in the command bar and selecting the desired users:
The user can change which users are being shown in the grid by selecting the users button in the command bar and selecting a desired users:
![Select users](./src/images/SelectUsersPopout.PNG)
The user can change which lists are being shown in the grid by selecting the lists button in the command bar and selecting the desired lists:
The user can change which lists are being shown in the grid by selecting the lists button in the command bar and selecting a desired lists:
![Select Lists](./src/images/Selectlistspopout.PNG)
@ -41,45 +39,38 @@ The user can change alternate between displaying user names and emails selectin
![Select Mode](./src/images/SelectDisplayModePopout.PNG)
The first configuration panel of the webpart is shown below:
The the first configuration panel of the webpart is shown below:
![config panel](./src/images/Configuration.PNG)
### Permission Settings
Permission Settings
The Permission Settings allow you to select which permissions to show in the grid and to select the Icon and color used to display the selected permission.
### User Settings
User Settings
The Show Email or Name Toggle determines whether the name or email is displayed by default.
The Show Security Groups checkbox determines whether SharePoint Security groups are included in the grid.
The Show Users checkbox determines whether Users are included in the grid.
The Only show users with permissions toggle determines whether the grid should display all users with access to the web, or only users with the selected permission
The Only show users with permissions toggle determines whether the grid shold diplay all users with access to the web, or only users with the selected permission
![config panel](./src/images/Permissions.gif)
The Let Users Select users checkbox determines whether Users can filter the selected users in the grid.
### Display Settings
Display Settings
The Initial Title column width determines the initial width of the Title column(it can be resized).
The second configuration panel allows the owner to configure the List Settings
![List Configuration panel](./src/images/ListConfiguration.PNG)
The second configuarion pannel allows the owner to configure the List Settings
![List Confoguration panel](./src/images/ListConfiguration.PNG)
### List Settings
List Settings
The Show Hidden Lists checkbox determines whether Hidden lists are displayed.
The Show System Lists checkbox determines whether System Lists (Catalogs) are included in the grid.
The Show Users checkbox determines whether Users are included in the grid.
The Let Users Select lists checkbox determines whether Users can filter the selected lists in the grid.
### Select Lists
Select Lists
The Include/Exclude Selected lists Toggle determines whether the lists selected are to be included or excluded.
@ -112,7 +103,7 @@ Solution|Author(s)
Version|Date|Comments
-------|----|--------
1.0.0.2|April 5, 2020| Updates to SPFx 1.10; Allow display of multiple permissions
1.0.0.2|April 5, 2021| Updates to SPFx 1.10; Allow display of multiple permissions
1.0.0.1|April 25, 2018|Update to SPFx 1.4.1
1.0.0.0|December 31, 2016|Initial version