Merge branch 'dev'

This commit is contained in:
VesaJuvonen 2018-12-28 17:51:54 +02:00
commit 50ca5fdb85
59 changed files with 19743 additions and 88 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-birthdays/.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,12 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.7.0",
"libraryName": "happy-birdthay",
"libraryId": "57890dd1-b655-4ec8-85ec-e47a9b696e7c",
"environment": "spo",
"packageManager": "npm",
"isCreatingSolution": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,76 @@
# React Birthdays Web Part
## Summary
The Web Part Birthdays shows the upcoming birthdays in the company, the web part reads birthdays from a list located on the tenant's home site with title "Birthdays."
There is an Azure function available that get AAD user birthdays, this function creates a list on the tenant root site, if it does not exist.
See the local.settings.json for details on the required application variable located in SyncUsersBirthdaysFunction folder.
But you can synchronize the Birthdays list with other applications HR Systems, or other sources
![Brithdays Web Part](./assets/birthdays.png)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
> Update accordingly as needed.
## Prerequisites
Existing list in tenant root site, with the Title "Birthdays" and columns:
Column Internal Name|Type|Required| comments
--------------------|----|--------|----------
jobTitle| Text| no|
Birthday| DateTime | true|
userAADGUID| Text| no | required if used Azure Function to get Birthdays from AAD
Title| Text| true
## Solution
Solution|Author(s)
--------|---------
react Birthday Web Part|João Mendes
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|November 6, 2018|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 build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
## Features
This project contains sample Birthday web parts built on the SharePoint Framework using React
and an Azure Function to get user Birthdays from AAD.
This sample illustrates the following concepts on top of the SharePoint Framework:
- using React for building SharePoint Framework client-side web parts
- using React components for building Birthday web part
- using MSGraph API to get data from SharePoint Lists
- using MSGraph API to read users from AAD
- using @PnP/PnPjs to create a List, add, update, delete Items.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/readme-template" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 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": {
"happy-birdthay-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/Birthdays/BirthdaysWebPart.js",
"manifest": "./src/webparts/Birthdays/BirthdaysWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ControlStrings": "lib/controls/HappyBirthdayCard/loc/{locale}.js",
"BirthdaysWebPartStrings": "lib/webparts/Birthdays/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"includeExtensions": [
"png",
"jpg",
"svg"
]
}

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": "happy-birdthay",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,15 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "happy-birdthay-client-side-solution",
"id": "57890dd1-b655-4ec8-85ec-e47a9b696e7c",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/birdthays.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 -->"
}

10
samples/react-birthdays/gulpfile.js vendored Normal file
View File

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

17777
samples/react-birthdays/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "Birthdays",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/loader-set-webpack-public-path": "^3.2.102",
"@microsoft/sp-core-library": "1.7.0",
"@microsoft/sp-lodash-subset": "1.7.0",
"@microsoft/sp-office-ui-fabric-core": "1.7.0",
"@microsoft/sp-webpart-base": "1.7.0",
"@pnp/common": "^1.2.5",
"@pnp/graph": "^1.2.5",
"@pnp/logging": "^1.2.5",
"@pnp/odata": "^1.2.5",
"@pnp/sp": "^1.2.5",
"@pnp/spfx-controls-react": "1.10.0",
"@pnp/spfx-property-controls": "1.12.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.4.2",
"@types/react-dom": "16.0.5",
"@types/webpack-env": "1.13.1",
"moment": "^2.22.2",
"react": "16.3.2",
"react-dom": "16.3.2"
},
"devDependencies": {
"@microsoft/set-webpack-public-path-plugin": "^2.1.58",
"@microsoft/sp-build-web": "1.7.0",
"@microsoft/sp-module-interfaces": "1.7.0",
"@microsoft/sp-tslint-rules": "1.7.0",
"@microsoft/sp-webpart-workbench": "1.7.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

View File

@ -0,0 +1,11 @@
{
"disabled": false,
"bindings": [
{
"name": "myTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 */5 * * * *"
}
]
}

View File

@ -0,0 +1,3 @@
{
"version": "2.0"
}

View File

@ -0,0 +1,329 @@
const request = require('request-promise');
const sp = require('@pnp/sp').sp;
const Web = require('@pnp/sp').Web;
const SPFetchClient = require("@pnp/nodejs").SPFetchClient;
const moment = require('moment');
const tenantId = GetEnvironmentVariable('TenantId');
const tenantUrl = GetEnvironmentVariable('TenantUrl');
const clientId = GetEnvironmentVariable('GraphClientId');
const clientSecret = GetEnvironmentVariable('GraphClientSecret');
const birthdayListTitle = GetEnvironmentVariable('BirthdayListTitle');
let _context;
let _msgraphToken;
let _deltaLink;
let _appCatalog;
module.exports = async function (context, myTimer) {
// Main
var timeStamp = new Date().toISOString();
if (myTimer.isPastDue) {
context.log('User Birthdays Sync is running late!');
}
context.log(`User Birthdays Sync started at ${timeStamp}`);
try {
// onInit function
await onInit(context, myTimer);
const result = await ensureBirthdaysList();
// Read Users
if (result) {
await getAllUsers(_deltaLink);
}
else {
context.log(`Error Create or Access Birthday List ${timeStamp}`);
// process.exit(1);
}
// End
context.log(`User Birthdays Sync ended at ${timeStamp}`);
process.exit(0);
}
catch (error) {
context.log(error);
// process.exit(1);
}
};
// Get Enviroment Variavel
function GetEnvironmentVariable(name) {
return process.env[name];
}
// Get MSGraph Token
async function GetMSGraphToken() {
let access_token = null;
const payload = `client_id=${clientId}&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret=${clientSecret}&grant_type=client_credentials`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
uri: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
body: payload,
json: true
};
// Request
try {
const result = await request(options)
if (result && result.access_token) {
access_token = result.access_token;
}
} catch (e) {
_context.log(`Error getting AccessToken, error: ${e}`);
}
return access_token;
}
// Get users
async function GetUsers(uri) {
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
"Authorization": `Bearer ${_msgraphToken}`
},
uri: uri,
json: true
};
return users = request(options)
}
// Get Birthday of User
async function GetUsersBirthday(userId) {
let results = null;
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
"Authorization": `Bearer ${_msgraphToken}`
},
uri: `https://graph.microsoft.com/v1.0/users/${userId}/birthday`,
json: true
};
try {
results = await request(options);
} catch (e) {
_context.log(`error getting birthday date for user ${userId}`)
}
// return value
return results.value ? results.value : null;
}
// Read all Users
async function getAllUsers(uri) {
try {
// get users 'https://graph.microsoft.com/v1.0/users/delta?$select=displayName,jobTitle,mail,Id'; or nextLink URL
const _users = await GetUsers(uri);
// has data?
if ( _users && _users.value && _users.value.length === 0 ){
return;
}
// get deltaLink for track changes.
// get nextLink to get next page
const _nextLink = (typeof _users["@odata.nextLink"] !== undefined) ? _users["@odata.nextLink"] : undefined;
const _deltaLink = (typeof _users["@odata.deltaLink"] !== undefined) ? _users["@odata.deltaLink"] : undefined;
// Read Users
for (const user of _users.value) {
_context.log(user.displayName);
// If user was removed from AAD
try {
if (user['@removed']) {
await deleteUser(user);
continue;
}
_birthday = await GetUsersBirthday(user.id);
const _year = moment(_birthday.toString()).format('YYYY');
// The Birthday Date has year 2000
if (_year === '2000') {
// check if user exists
_exists = await checkUserExist(user);
if (!_exists) {
// Add user to List
await addUser(user, _birthday)
} else {
//Update user
await updateUser(user, _birthday)
}
}
} catch (error) {
_context.log(`Error Adding or Updating users : ${error} `);
}
}
try {
// Load next Page
if (_nextLink) {
await getAllUsers(_nextLink);
}
// deltaLink exist (last request)
if (_deltaLink) {
// Save Tenant property with deltaLink for track changes
await _appCatalog.setStorageEntity("UserBirthdayDeltaLink", _deltaLink, "Users Sync Delta Token");
}
} catch (error) {
_context.log(`Error updating StorageEntity : ${error} `);
}
} catch (error) {
_context.log(`Error on read users : ${error} `);
}
return ;
}
// onInit Function
async function onInit(context, myTimer) {
_context = context;
try {
// setup PnPJs
sp.setup({
sp: {
fetchClientFactory: () => {
return new SPFetchClient(
tenantUrl,
clientId,
clientSecret);
},
},
});
// Get MSGraph Token
_msgraphToken = await GetMSGraphToken();
// Get last deltaLink from Tenant Property
_appCatalog = new Web(`${tenantUrl}/sites/appcatalog`);
const _deltaLinkValue = await _appCatalog.getStorageEntity('UserBirthdayDeltaLink');
_deltaLink = _deltaLinkValue.Value ? _deltaLinkValue.Value : 'https://graph.microsoft.com/v1.0/users/delta?$select=displayName,jobTitle,mail,Id';
_context.log(_deltaLink);
} catch (error) {
context.log(`Error on onInit function: ${error}`);
}
return;
}
// Add User to SP List
async function addUser(user, _birthday) {
let _item = null;
try {
_item = await sp.web.lists.getByTitle(birthdayListTitle).items.add({
Title: user.displayName,
jobTitle: user.jobTitle ? user.jobTitle : '',
email: user.mail ? user.mail : '',
userAADGUID: user.id,
Birthday: moment(_birthday.toString()).format('YYYY-MM-DD')
});
} catch (e) {
__context.log(`Èrror adding user ${user.displayName}, error: ${e}`);
}
return _item ? _item : null;
}
// Update User Data
async function updateUser(user, _birthday) {
let _userUpdated = null;
const _item = await sp.web.lists.getByTitle(birthdayListTitle).items.top(1).filter(`userAADGUID eq '${user.id}'`).get()
if (_item && _item.length > 0) {
_userUpdated = await sp.web.lists.getByTitle(birthdayListTitle).items.getById(_item[0].Id).update({
Title: user.displayName,
jobTitle: user.jobTitle ? user.jobTitle : '',
email: user.mail ? user.mail : '',
userAADGUID: user.id,
Birthday: moment(_birthday.toString()).format('YYYY-MM-DD')
});
}
// return value
return _userUpdated ? _userUpdated : null;
}
// check if user Exist in SPList
async function checkUserExist(user) {
let _item = null;
try {
_item = await sp.web.lists.getByTitle(birthdayListTitle).items.top(1).filter(`userAADGUID eq '${user.id}'`).get()
} catch (e) {
__context.log(`Error Checking if user ${user.displayName} exists, error: ${e} `);
}
// Return Value
return _item && _item.length > 0 ? true : false;
}
// Delete User from List
async function deleteUser(user) {
let _item = null;
try {
_item = await sp.web.lists.getByTitle(birthdayListTitle).items.top(1).filter(`userAADGUID eq '${user.id}'`).get()
if (_item && _item.length > 0) {
try {
await sp.web.lists.getByTitle(birthdayListTitle).items.getById(_item[0].Id).delete();
} catch (e) {
_context.log(`Error deleting user ${user.displayName}, error: ${e}`);
}
}
} catch (e) {
_context.log(`Error get user ${user.displayName} to delete, error: ${e}`);
}
}
// this method ensures that the birthdays lists exists, or if it doesn't exist create it
async function ensureBirthdaysList() {
let _web = new Web(tenantUrl);
let result = false;
try {
const ensureResult = await _web.lists.ensure(birthdayListTitle,
"Birthdays 2",
100,
true);
// if we've got the list
if (ensureResult.list != null) {
// if the list has just been created
if (ensureResult.created) {
// we need to add the custom fields to the list
const jobTitleFieldAddResult = await ensureResult.list.fields.addText(
"jobTitle", 100,
{ Required: false });
await jobTitleFieldAddResult.field.update({ Title: "Job Title" });
const emailFieldAddResult = await ensureResult.list.fields.addText(
"email", 100,
{ Required: true });
await emailFieldAddResult.field.update({ Title: "Email" });
const BirthdayFieldAddResult = await ensureResult.list.fields.addDateTime(
"Birthday",
);
await BirthdayFieldAddResult.field.update({ Title: "Birthday" });
const userAADGUIDFieldAddResult = await ensureResult.list.fields.addText(
"userAADGUID", 100,
{ Required: true });
await userAADGUIDFieldAddResult.field.update({ Title: "AAD ID " });
await ensureResult.list.fields.getByInternalNameOrTitle('Title').update({ Title: 'Display Name' });
// the list is ready to be used
result = true;
} else {
// the list already exists, check the fields
try {
const jobTitleField = await ensureResult.list.fields.getByInternalNameOrTitle("jobTitle").get();
const emailField = await ensureResult.list.fields.getByInternalNameOrTitle("email").get();
const BirthdayField = await ensureResult.list.fields.getByInternalNameOrTitle("Birthday").get();
const userAADGUIDField = await ensureResult.list.fields.getByInternalNameOrTitle("userAADGUID").get();
// if it is ok, then the list is ready to be used
result = true;
} catch (e) {
// if any of the fields does not exist, write exception in the _context log
_context.log(`The ${birthdayListTitle} list does not match the expected fields definition. error ${e}`);
result = false;
}
}
}
} catch (e) {
// if we fail to create the list, write an exception in the _context log
_context.log(`Failed to create birthdays list ${birthdayListTitle}.`);
result = false;
}
return result;
}

View File

@ -0,0 +1,10 @@
{
"IsEncrypted": false,
"Values": {
"GraphClientId": "",
"GraphClientSecret": "",
"TenantId": "",
"TenantUrl": "https://contoso.sharepoint.com",
"BirthdayListTitle": "Birthdays"
}
}

View File

@ -0,0 +1,11 @@
# TimerTrigger - JavaScript
The `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.
## How it works
For a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: "When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year".
## Learn more
<TODO> Documentation

View File

@ -0,0 +1,184 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.happyBirdthay {
.documentCard {
-webkit-font-smoothing: antialiased;
background-color: #ffffff;
border: 2px solid #eaeaea;
-webkit-box-sizing: border-box;
box-sizing: border-box;
max-width: 320px;
min-width: 200px;
min-height: 190px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
display: inline-block;
}
.documentCard:hover{
border-color: #c8c8c8;
}
.documentCardWrapper {
margin-top: 15px;
min-Width: 200px;
width: 200px;
min-height: 190px;
margin-Left: 15px;
position: relative;
text-align: center;
display: inline-block;
}
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.today{
@include ms-font-xl;
@include ms-fontWeight-semibold;
position: absolute;
top: 61px;
width: 100%;
font-style: italic;
text-align: top;
padding-bottom: 15px;
}
.centered {
@include ms-font-l;
@include ms-fontWeight-semibold;
position: absolute;
top: 78px;
width: 100%;
font-style: italic;
text-align: top;
color: #666666;
}
.personaContainer {
margin-Top: 40px;
margin-Left: 10px;
margin-Right: 10px;
}
.persona{
position: absolute;
top: 137px;
max-width: 200px;
}
.actions{
position: absolute;
top: 190px;
width: 100%;
border-top-style: solid;
border-top-color: #f4f4f4;
border-top-width: 1.1px;
// background-color: #f8f8f8;
}
.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;
}
.centeredHeader {
position: relative;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.birthdaycake {
font-Size: 20px;
width: 30px;
height: 30px;
color: #ff6f69;
// color: $ms-color-themeTertiary;
top: 60px;
position: absolute;
border-radius: 50%;
background-color: #eaeaea;
@include ms-fontWeight-semibold;
}
.displayBirthdayToday{
@include ms-fontWeight-semibold;
width: 100%;
//color: $ms-color-themeTertiary;
color: #ff6f69;
top: 107px;
position: absolute;
font-size: $ms-font-size-s;
}
.displayBirthday{
@include ms-fontWeight-semibold;
width: 100%;
color: $ms-color-themeTertiary;
top: 107px;
position: absolute;
font-size: $ms-font-size-s;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,126 @@
import * as React from 'react';
import styles from './HappyBirthdayCard.module.scss';
import { IHappyBirthdayCardProps } from './IHappyBirthdayCardProps';
import { IHappyBirthdayCardPState } from './IHappyBirthdayCardState';
import { escape } from '@microsoft/sp-lodash-subset';
import { IPersonaSharedProps, Persona, PersonaSize, IPersonaProps, PersonaPresence } from 'office-ui-fabric-react/lib/Persona';
import { Image, IImageProps, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { Label } from 'office-ui-fabric-react/lib/Label';
import * as strings from 'ControlStrings';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import * as moment from 'moment';
import {
DocumentCardActions,
} from 'office-ui-fabric-react/lib/DocumentCard';
const img: string = require('../../../assets/baloons.png');
const IMG_WIDTH: number = 200;
const IMG_HEIGTH: number = 190;
export class HappyBirthdayCard extends React.Component<IHappyBirthdayCardProps, IHappyBirthdayCardPState> {
private _Persona: IPersonaSharedProps;
private _birthdayMsg: string = '';
constructor(props: IHappyBirthdayCardProps) {
super(props);
const photo: string = `/_layouts/15/userphoto.aspx?size=L&username=${this.props.userEmail}`;
console.log(photo);
this._Persona = {
imageUrl: photo ? photo : '',
imageInitials: this._getInitial(this.props.userName),
text: this.props.userName,
secondaryText: this.props.jobDescription,
tertiaryText: this.props.birthday,
};
this.state = {
isBirthdayToday: this._birthdayIsToday(this.props.birthday)
};
this._onRenderTertiaryText = this._onRenderTertiaryText.bind(this);
this._getInitial = this._getInitial.bind(this);
this._birthdayIsToday = this._birthdayIsToday.bind(this);
}
// Render
public render(): React.ReactElement<IHappyBirthdayCardProps> {
this._birthdayMsg = this.state.isBirthdayToday ? strings.HappyBirthdayMsg : strings.NextBirthdayMsg;
return (
<div className={styles.happyBirdthay}>
<div className={styles.documentCardWrapper}>
<div className={styles.documentCard}>
<Image
imageFit={ImageFit.cover}
src={img}
width={IMG_WIDTH}
height={IMG_HEIGTH}
/>
<Label className={styles.centered} >{this._birthdayMsg}</Label>
{
this.state.isBirthdayToday ?
<Label className={styles.displayBirthdayToday}>{this.props.birthday}</Label>
:
<Label className={styles.displayBirthday}>{this.props.birthday}</Label>
}
{
this.state.isBirthdayToday ?
<Icon iconName="BirthdayCake" className={styles.birthdaycake} />
:
''
}
<div className={styles.personaContainer}>
<Persona
{...this._Persona}
size={PersonaSize.regular}
className={styles.persona}
onRenderTertiaryText={this._onRenderTertiaryText}
/>
</div>
<div className={styles.actions}>
<DocumentCardActions
actions={[
{
iconProps: { iconName: 'Mail' },
onClick: (ev: any) => {
ev.preventDefault();
ev.stopPropagation();
window.location.href = `mailto:${this.props.userEmail}?subject=${this._birthdayMsg}!`;
},
ariaLabel: 'email'
}
]}
/>
</div>
</div>
</div>
</div>
);
}
// Today is Birthday ?
private _birthdayIsToday(birthday: string): boolean {
const _todayDay = moment().date();
const _todayMonth = moment().month() + 1;
const _birthdayDay = moment(birthday, 'Do MMM').date();
const _birthdayMonth = moment(birthday, 'Do MMM').month() + 1;
const _retvalue = (_todayDay === _birthdayDay && _todayMonth === _birthdayMonth) ? true : false;
return _retvalue;
}
// Get Initials
private _getInitial(userName: string): string {
const _arr = userName.split(' ');
const _initial = _arr[0].charAt(0).toUpperCase() + (_arr[1] ? _arr[1].charAt(0).toLocaleUpperCase() : "");
return _initial;
}
// Render tertiary text
private _onRenderTertiaryText = (props: IPersonaProps): JSX.Element => {
return (
<div>
<span className='ms-fontWeight-semibold' style={{ color: '#71afe5' }}>
{props.tertiaryText}</span>
</div>
);
}
}
export default HappyBirthdayCard;

View File

@ -0,0 +1,7 @@
export interface IHappyBirthdayCardProps {
userName?:string;
jobDescription?: string;
birthday: string;
userEmail:string;
congratulationsMsg?: string;
}

View File

@ -0,0 +1,4 @@
export interface IHappyBirthdayCardPState {
isBirthdayToday: boolean;
}

View File

@ -0,0 +1,4 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler
export * from './IHappyBirthdayCardProps';
export * from './IHappyBirthdayCardState';
export * from './HappyBirthdayCard';

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"BirdthayControlDefaultDay": "Today",
"HappyBirthdayMsg": "Happy Birthday!",
"NextBirthdayMsg": "Next Birthday",
"MessageNoBirthdays": "There are no birthdays for the next days."
}
});

View File

@ -0,0 +1,11 @@
declare interface IControlStrings {
BirdthayControlDefaultDay: string,
HappyBirthdayMsg: string,
NextBirthdayMsg: string,
MessageNoBirthdays: string
}
declare module 'ControlStrings' {
const strings: IControlStrings;
export = strings;
}

View File

@ -0,0 +1,145 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.happyBirthday {
.backgroundImgBallons{
text-align: center;
text-align: -webkit-center;
opacity: 0.23;
}
.documentCard {
-webkit-font-smoothing: antialiased;
background-color: #ffffff;
border: 1px solid #eaeaea;
-webkit-box-sizing: border-box;
box-sizing: border-box;
max-width: 320px;
min-width: 200px;
min-height: 190px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
}
.documentCard:hover{
border-color: #c8c8c8;
}
.documentCardWrapper {
margin-top: 15px;
min-Width: 200px;
width: 200px;
min-height: 190px;
margin-Left: 15px;
position: relative;
text-align: center;
}
.container {
display: inline-block;
max-width: 100%;
margin: 0px auto;
}
.today{
@include ms-font-xl;
@include ms-fontWeight-semibold;
position: absolute;
top: 75px;
width: 100%;
font-style: italic;
text-align: top;
padding-bottom: 15px;
}
.centered {
@include ms-font-xl;
@include ms-fontWeight-semibold;
position: absolute;
top: 100px;
width: 100%;
font-style: italic;
text-align: top;
}
.persona{
position: absolute;
top: 170px;
max-width: 236px;
}
.actions{
position: absolute;
top: 290px;
width: 100%;
border-top-style: solid;
border-top-color: #f4f4f4;
border-top-width: 1.1px;
// background-color: #f8f8f8;
}
.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;
}
.centeredHeader {
position: relative;
}
.subTitle {
@include ms-font-l;
// @include ms-fontColor-white;
@include ms-fontWeight-semibold;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,47 @@
import * as React from 'react';
import styles from './HappyBirthday.module.scss';
import { IHappyBirthdayProps } from './IHappyBirthdayProps';
import { IHappbirthdayState } from './IHappybirthdayState';
import { escape } from '@microsoft/sp-lodash-subset';
import { IUser } from './IUser';
import HappyBirdthayCard from '../../controls/happyBirthdayCard/HappyBirthdayCard';
import * as moment from 'moment';
import { Image, IImageProps, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { Label } from 'office-ui-fabric-react/lib/Label';
import * as strings from 'ControlStrings';
export class HappyBirthday extends React.Component<IHappyBirthdayProps, IHappbirthdayState> {
private _showBirthdays: boolean = true;
constructor(props: IHappyBirthdayProps) {
super(props);
}
public async componentDidMount() {
}
public componentDidUpdate(prevProps: IHappyBirthdayProps, prevState: IHappbirthdayState): void {
}
//
public render(): React.ReactElement<IHappyBirthdayProps> {
return (
<div className={styles.happyBirthday}>
{
this.props.users.map((user: IUser) => {
return (
<div className={styles.container}>
<HappyBirdthayCard userName={user.userName}
jobDescription={user.jobDescription}
birthday={moment(user.birthday, ["MM-DD-YYYY", "YYYY-MM-DD", "DD/MM/YYYY", "MM/DD/YYYY"]).format('Do MMMM')}
userEmail={user.userEmail}
/>
</div>
);
})
}
</div>
);
}
}
export default HappyBirthday;

View File

@ -0,0 +1,5 @@
import { IUser } from './IUser';
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IHappyBirthdayProps {
users: IUser[];
}

View File

@ -0,0 +1,2 @@
export interface IHappbirthdayState{
}

View File

@ -0,0 +1,7 @@
export interface IUser {
key: string;
userName:string;
jobDescription?: string;
birthday: string;
userEmail: string;
}

View File

@ -0,0 +1,5 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler
export * from './IHappyBirthdayProps';
export * from './IHappybirthdayState';
export * from './IUser';
export * from './HappyBirthday';

View File

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

View File

@ -0,0 +1,7 @@
export interface IUser {
key: string;
userName:string;
jobDescription?: string;
birthday: string;
userEmail: string;
}

View File

@ -0,0 +1,55 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { ApplicationCustomizerContext } from "@microsoft/sp-application-base";
import { SPHttpClient, SPHttpClientResponse, MSGraphClient } from "@microsoft/sp-http";
import * as moment from 'moment';
export class SPService {
private graphClient: MSGraphClient = null;
private birthdayListTitle: string = "Birthdays";
constructor(private _context: WebPartContext | ApplicationCustomizerContext) {
}
// Get Profiles
public async getPBirthdays(upcommingDays: number): Promise<any[]> {
let _results, _today: string, _month: string, _day: number;
let _filter: string, _countdays: number, _f:number, _nextYearStart: string;
let _FinalDate: string;
try {
_results = null;
_today = '2000-' + moment().format('MM-DD');
_month = moment().format('MM');
_day = parseInt(moment().format('DD'));
_filter = "fields/Birthday ge '" + _today + "'";
// If we are in Dezember we have to look if there are birthday in January
// we have to build a condition to select birthday in January based on number of upcommingDays
// we can not use the year for teste , the year is always 2000.
console.log(_month);
if (_month === '12') {
_countdays = _day + upcommingDays;
_f = 0;
_nextYearStart = '2000-01-01';
_FinalDate = '2000-01-';
if ((_countdays) > 31) {
_f = _countdays - 31;
_FinalDate = _FinalDate + _f;
_filter = "fields/Birthday ge '" + _today + "' or (fields/Birthday ge '" + _nextYearStart + "' and fields/Birthday le '" + _FinalDate + "')";
}
}
this.graphClient = await this._context.msGraphClientFactory.getClient();
_results = await this.graphClient.api(`sites/root/lists('${this.birthdayListTitle}')/items`)
.version('v1.0')
.expand('fields')
.top(upcommingDays)
.filter(_filter)
.get();
return _results.value;
} catch (error) {
console.dir(error);
return Promise.reject(error);
}
}
}
export default SPService;

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "e629ef30-a9ec-4713-b39a-2cfa8b323902",
"alias": "BirthdaysWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "Birthdays Web Part" },
"description": { "default": "Birthdays Web Part" },
"officeFabricIconFontName": "Page",
"properties": {
"title": "Birthdays",
"numberUpcomingDays": 5
}
}]
}

View File

@ -0,0 +1,87 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { PropertyFieldNumber } from '@pnp/spfx-property-controls/lib/PropertyFieldNumber';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'BirthdaysWebPartStrings';
import Birdthays from './components/Birthdays';
import { IBirthdaysProps } from './components/IBirthdaysProps';
import { MSGraphClient } from '@microsoft/sp-http';
export interface IBirthdaysWebPartProps {
title: string;
numberUpcomingDays: number;
}
export default class BirthdaysWebPart extends BaseClientSideWebPart<IBirthdaysWebPartProps> {
private graphCLient: MSGraphClient;
public onInit(): Promise<void> {
return super.onInit().then(_ => {
// other init code may be present
});
}
public render(): void {
const element: React.ReactElement<IBirthdaysProps > = React.createElement(
Birdthays,
{
title: this.properties.title,
numberUpcomingDays: this.properties.numberUpcomingDays,
context: this.context,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.title = value;
}
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('title', {
label: strings.DescriptionFieldLabel
}),
PropertyFieldNumber("numberUpcomingDays", {
key: "numberUpcomingDays",
label: strings.NumberUpComingDaysLabel,
description: strings.NumberUpComingDaysLabel,
value: this.properties.numberUpcomingDays,
maxValue: 10,
minValue: 5,
disabled: false
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,144 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.happyBirthday {
.backgroundImgBallons{
text-align: center;
text-align: -webkit-center;
opacity: 0.21;
}
.documentCard {
-webkit-font-smoothing: antialiased;
background-color: #ffffff;
border: 1px solid #eaeaea;
-webkit-box-sizing: border-box;
box-sizing: border-box;
max-width: 100%;
min-width: 236px;
min-height: 300px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
}
.documentCardWrapper {
margin-top: 15px;
min-Width: 238px;
width: 238px;
min-height: 300px;
margin-Left: 12px;
position: relative;
text-align: center;
}
.container {
display: inline-block;
max-width: 100%;
margin: 0px auto;
}
.today{
@include ms-font-xl;
@include ms-fontWeight-semibold;
position: absolute;
top: 75px;
width: 100%;
font-style: italic;
text-align: top;
padding-bottom: 15px;
}
.centered {
@include ms-font-xl;
@include ms-fontWeight-semibold;
position: absolute;
top: 100px;
width: 100%;
font-style: italic;
text-align: top;
}
.persona{
position: absolute;
top: 170px;
max-width: 236px;
}
.actions{
position: absolute;
top: 290px;
width: 100%;
border-top-style: solid;
border-top-color: #f4f4f4;
border-top-width: 1.1px;
// background-color: #f8f8f8;
}
.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;
}
.centeredHeader {
position: relative;
}
.subTitle {
@include ms-font-m;
@include ms-fontWeight-semibold;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,117 @@
import * as React from 'react';
import styles from './Birthdays.module.scss';
import { IBirthdaysProps } from './IBirthdaysProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { HappyBirthday, IUser } from '../../../controls/happybirthday';
import * as moment from 'moment';
import { IBirthdayState } from './IBirthdaysState';
import SPService from '../../../services/SPService';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
const imgBackgroundBallons: string = require('../../../../assets/ballonsBackgroud.png');
import { Image, IImageProps, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { Label } from 'office-ui-fabric-react/lib/Label';
import * as strings from 'ControlStrings';
export default class Birthdays extends React.Component<IBirthdaysProps, IBirthdayState> {
private _users: IUser[] = [];
private _spServices: SPService;
constructor(props: IBirthdaysProps) {
super(props);
this._spServices = new SPService(this.props.context);
this.state = {
Users: [],
showBirthdays: true
};
}
public componentDidMount(): void {
this.GetUsers();
}
public componentDidUpdate(prevProps: IBirthdaysProps, prevState: IBirthdayState): void {
}
// Render
public render(): React.ReactElement<IBirthdaysProps> {
let _center: any = !this.state.showBirthdays ? "center" : "";
return (
<div className={styles.happyBirthday}
style={{ textAlign: _center }} >
<div className={styles.container}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty} />
{
!this.state.showBirthdays ?
<div className={styles.backgroundImgBallons}>
<Image imageFit={ImageFit.cover}
src={imgBackgroundBallons}
width={150}
height={150}
/>
<Label className={styles.subTitle}>{strings.MessageNoBirthdays}</Label>
</div>
:
<HappyBirthday users={this.state.Users}
/>
}
</div>
</div>
);
}
// Sort Array of Birthdays
private SortBirthdays(users: IUser[]) {
return users.sort( (a, b) => {
if (a.birthday > b.birthday) {
return 1;
}
if (a.birthday < b.birthday) {
return -1;
}
return 0;
});
}
// Load List Of Users
private async GetUsers() {
let _otherMonthsBirthdays: IUser[], _dezemberBirthdays: IUser[];
const listItems = await this._spServices.getPBirthdays(this.props.numberUpcomingDays);
if (listItems && listItems.length > 0) {
_otherMonthsBirthdays = [];
_dezemberBirthdays = [];
for (const item of listItems) {
this._users.push({ key: item.fields.email, userName: item.fields.Title, userEmail: item.fields.email, jobDescription: item.fields.JobTitle, birthday: item.fields.Birthday });
}
// Sort Items by Birthday MSGraph List Items API don't support ODATA orderBy
// for end of year teste and sorting
// first select all bithdays of Dezember to sort this must be the first to show
if (moment().format('MM') === '12') {
_dezemberBirthdays = this._users.filter( (v) => {
var _currentMonth = moment(v.birthday, ["MM-DD-YYYY", "YYYY-MM-DD", "DD/MM/YYYY", "MM/DD/YYYY"]).format('MM');
return (_currentMonth === '12');
});
// Sort by birthday date in Dezember month
_dezemberBirthdays = this.SortBirthdays(_dezemberBirthdays);
// select birthdays != of month 12
_otherMonthsBirthdays = this._users.filter((v) => {
var _currentMonth = moment(v.birthday, ["MM-DD-YYYY", "YYYY-MM-DD", "DD/MM/YYYY", "MM/DD/YYYY"]).format('MM');
return (_currentMonth !== '12');
});
// sort by birthday date
_otherMonthsBirthdays = this.SortBirthdays(_otherMonthsBirthdays);
// Join the 2 arrays
this._users = _dezemberBirthdays.concat(_otherMonthsBirthdays);
}
else {
this._users = this.SortBirthdays(this._users);
}
}
// this._users=[];
this.setState(
{
Users: this._users,
showBirthdays: this._users.length === 0 ? false : true
});
}
}

View File

@ -0,0 +1,9 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from '@microsoft/sp-core-library';
export interface IBirthdaysProps {
title: string;
numberUpcomingDays: number;
context: WebPartContext;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
}

View File

@ -0,0 +1,5 @@
import {IUser } from '../../../services/IUser';
export interface IBirthdayState{
Users : IUser[] ;
showBirthdays: boolean;
}

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneDescription": "Happy Brithday",
"BasicGroupName": "Properties",
"DescriptionFieldLabel": "Title",
"NumberUpComingDaysLabel": 'Number of upcomming birthdays'
}
});

View File

@ -0,0 +1,11 @@
declare interface IBirthdayWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
NumberUpComingDaysLabel: string;
}
declare module 'BirthdaysWebPartStrings' {
const strings: IBirthdayWebPartStrings;
export = strings;
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

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

View File

@ -27,3 +27,27 @@
.hoverIcon {
color: '[theme:themeDarker, default:#0078d7]';
}
.tags {
margin-top: 5px;
display: flex;
align-items: center;
* + * {
margin-left: 3px;
}
a, a:visited, a:focus, a:link, a:active {
color: '[theme:themePrimary, default:#0078d7]';
}
a:not(:empty):after {
content: ",";
display: inline-block;
}
a:not(:empty):last-child:after {
content: "";
}
}

View File

@ -39,79 +39,80 @@ abstract class BaseTemplateService {
public static getListDefaultTemplate(): string {
return html`
<style>
.template_listItem {
display:flex;
display: -ms-flexbox;
padding: 10px;
justify-content: space-between;
}
.template_listItem {
display:flex;
display: -ms-flexbox;
padding: 10px;
justify-content: space-between;
}
.template_listItem img.img-preview {
width: 120px;
opacity: 1;
display: block;
height: auto;
transition: .5s ease;
backface-visibility: hidden;
}
.template_listItem img.img-preview {
width: 120px;
opacity: 1;
display: block;
height: auto;
transition: .5s ease;
backface-visibility: hidden;
}
.template_result {
display: flex;
display: -ms-flexbox;
}
.template_result {
display: flex;
display: -ms-flexbox;
}
.template_listItem iframe, .template_listItem .video-js {
height: 250px;
margin: 10px;
}
.template_listItem iframe, .template_listItem .video-js {
height: 250px;
margin: 10px;
}
.template_contentContainer {
display: flex;
width: 100%;
display: -ms-flexbox;
flex-direction: column;
margin-right: 15px;
}
.template_contentContainer {
display: flex;
width: 100%;
display: -ms-flexbox;
flex-direction: column;
margin-right: 15px;
}
.template_previewContainer {
align-items: center;
display: flex;
display: -ms-flexbox;
}
.template_previewContainer {
align-items: center;
display: flex;
display: -ms-flexbox;
}
/* Width for the documents and videos preview */
.videoPreview, .iframePreview {
width: 400px;
}
/* Width for the documents and videos preview */
.videoPreview, .iframePreview {
width: 400px;
}
.template_icon {
height: 32px;
margin-right: 15px;
}
.template_icon {
height: 32px;
margin-right: 15px;
}
.hover {
transition: .5s ease;
opacity: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
}
.hover {
transition: .5s ease;
opacity: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
}
.img-container {
position: relative;
}
.img-container {
position: relative;
}
.img-container:hover img {
opacity: 0.2;
}
.img-container:hover img {
opacity: 0.2;
}
.img-container:hover .hover {
opacity: 1;
}
.img-container:hover .hover {
opacity: 1;
}
</style>
<div class="template_root">
{{#if showResultsCount}}
@ -146,6 +147,14 @@ abstract class BaseTemplateService {
<span class=""><a href="{{getUrl item}}">{{Title}}</a></span>
<span class="">{{getSummary HitHighlightedSummary}}</span>
<span class=""><span>{{getDate Created "LL"}}</span></span>
<div class="${templateStyles.tags}">
{{#if owstaxidmetadataalltagsinfo}}
<i class="ms-Icon ms-Icon--Tag" aria-hidden="true"></i>
{{#each (split owstaxidmetadataalltagsinfo ";") as |tag| }}
<a href="#owstaxidmetadataalltagsinfo:{{getLabel tag}}">{{getLabel tag}}</a>
{{/each}}
{{/if}}
</div>
</div>
</div>
<div class="template_previewContainer ms-hiddenSm">
@ -384,6 +393,19 @@ abstract class BaseTemplateService {
}
return result.length;
});
// Return the text label from amn'owstaxid_' type managed property
// <p>{{getLabel "L0|#045686734-5215-4aad-bed7-8c3f0dbb61fc|Document"}}</p>
Handlebars.registerHelper("getLabel", (owsTaxIdValue: string) => {
let termLabel = owsTaxIdValue;
const matches = /L0\|#.+\|(.*)/.exec(owsTaxIdValue);
if (matches) {
termLabel = matches[1];
}
return termLabel;
});
}
/**

View File

@ -44,9 +44,13 @@
{
"refinerName": "Size",
"displayValue": "Size of the file"
},
{
"refinerName": "owstaxidmetadataalltagsinfo",
"displayValue": "Tags"
}
],
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary,FileType,contentclass,ServerRedirectedEmbedURL,DefaultEncodingURL",
"selectedProperties": "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary,FileType,contentclass,ServerRedirectedEmbedURL,DefaultEncodingURL,owstaxidmetadataalltagsinfo",
"enableQueryRules": false,
"maxResultsCount": 10,
"showBlank": true,

View File

@ -246,6 +246,10 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
{
refinerName: "Size",
displayValue: "Size of the file"
},
{
refinerName: "owstaxidmetadataalltagsinfo",
displayValue: "Tags"
}
];
this.properties.sortList = Array.isArray(this.properties.sortList) ? this.properties.sortList : [
@ -259,7 +263,7 @@ export default class SearchResultsWebPart extends BaseClientSideWebPart<ISearchR
}
];
this.properties.sortableFields = Array.isArray(this.properties.sortableFields) ? this.properties.sortableFields : [];
this.properties.selectedProperties = this.properties.selectedProperties ? this.properties.selectedProperties : "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary,FileType,contentclass,ServerRedirectedEmbedURL,DefaultEncodingURL";
this.properties.selectedProperties = this.properties.selectedProperties ? this.properties.selectedProperties : "Title,Path,Created,Filename,SiteLogo,PreviewUrl,PictureThumbnailURL,ServerRedirectedPreviewURL,ServerRedirectedURL,HitHighlightedSummary,FileType,contentclass,ServerRedirectedEmbedURL,DefaultEncodingURL,owstaxidmetadataalltagsinfo";
this.properties.maxResultsCount = this.properties.maxResultsCount ? this.properties.maxResultsCount : 10;
this.properties.resultTypes = Array.isArray(this.properties.resultTypes) ? this.properties.resultTypes : [];
}

View File

@ -9,6 +9,8 @@ export interface ISuggestedTeamMembersProps {
export interface ISuggestedTeamMembersState {
people: IPerson[];
userIsGroupOwner: boolean;
loading: boolean;
}
export interface IPerson {

View File

@ -5,6 +5,7 @@ import styles from './SuggestedTeamMembers.module.scss';
import { IPersonaProps, Persona, PersonaSize } from 'office-ui-fabric-react/lib/Persona';
import { IPersonaWithMenu } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePickerItems/PeoplePickerItem.types';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import {
CompactPeoplePicker,
@ -26,7 +27,7 @@ export interface IMembersPickerProps {
export interface IMembersPickerState {
peopleList: IPersonaProps[];
currentSelectedItems?: IPersonaProps[];
resultAddMembers: string;
resultAddMembers: string[];
}
const suggestionProps: IBasePickerSuggestionsProps = {
@ -60,7 +61,7 @@ export default class MembersPicker extends React.Component<IMembersPickerProps,
this.state = {
peopleList: peopleList,
currentSelectedItems: [],
resultAddMembers: null
resultAddMembers: []
};
}
@ -160,7 +161,15 @@ export default class MembersPicker extends React.Component<IMembersPickerProps,
onClick={() => { this._addGroupMembers(); }}
/>
<Label>{this.state.resultAddMembers}</Label>
{
this.state.resultAddMembers.map(s => {
let type: MessageBarType = MessageBarType.info;
if (s.indexOf("Error") >= 0) {
type = MessageBarType.error;
}
return <MessageBar messageBarType={type} isMultiline={false}>{s}</MessageBar>;
})
}
</div>
);
}
@ -190,12 +199,21 @@ export default class MembersPicker extends React.Component<IMembersPickerProps,
};
var response: GraphHttpClientResponse = await this.props.graphHttpClient.post('v1.0/$batch', GraphHttpClient.configurations.v1, options);
var responseJson: string = await response.json();
var responseJson: any = await response.json();
console.log(responseJson);
let responsesInfo: string[] = [];
responseJson.responses.forEach((r: any) => {
if (r.status === 204) {
responsesInfo.push(`User ${r.id} added succesfuly`);
} else {
responsesInfo.push(`Error adding User ${r.id}. Maybe the user is already a member`);
}
});
this.setState({
resultAddMembers: "Members added to the group successfully"
resultAddMembers: responsesInfo
});
}

View File

@ -8,6 +8,7 @@ import { DefaultButton, PrimaryButton, IButtonProps, ActionButton } from 'office
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Guid } from '@microsoft/sp-core-library';
import MembersPicker from './MembersPicker';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
export default class SuggestedTeamMembers extends React.Component<ISuggestedTeamMembersProps, ISuggestedTeamMembersState> {
@ -15,18 +16,41 @@ export default class SuggestedTeamMembers extends React.Component<ISuggestedTeam
super(props);
this.state = {
people: []
loading: true,
people: [],
userIsGroupOwner: false
};
}
public componentDidMount(): void {
this._getMyPeople().then(people => {
this.setState({
people: people
});
this._userIsOwner().then(isOwner => {
if (!isOwner) {
this.setState({
loading: false,
userIsGroupOwner: false
});
} else {
this._getMyPeople().then(people => {
this.setState({
loading: false,
people: people,
userIsGroupOwner: true
});
});
}
});
}
private async _userIsOwner(): Promise<boolean> {
const query: string = `v1.0/me/ownedObjects?$filter=id eq '${this.props.groupId}'`;
const response: GraphHttpClientResponse = await this.props.graphHttpClient.get(
query,
GraphHttpClient.configurations.v1);
return response.ok;
}
private async _getMyPeople(): Promise<IPerson[]> {
const query: string = "v1.0/me/people?$filter=personType/class eq 'Person'";
@ -51,17 +75,29 @@ export default class SuggestedTeamMembers extends React.Component<ISuggestedTeam
public render(): React.ReactElement<ISuggestedTeamMembersProps> {
if (this.state.people == null || this.state.people.length === 0) {
let title: string = '';
if (this.state.loading) {
return <div>Loading data...</div>;
}
if (!this.state.userIsGroupOwner) {
return <MessageBar messageBarType={MessageBarType.error} isMultiline={false}>You are not Owner of this Group</MessageBar>;
}
if (this.props.teamsContext) {
title = 'Team: ' + this.props.teamsContext.teamName;
} else {
title = 'Group: ' + this.props.groupId;
}
const headerTitle = "These are suggested members to add to the " + title + "...";
return <div className={styles.suggestedTeamMembers}>
<p>These are suggested members to add to the group...</p>
<MembersPicker
people = {this.state.people}
groupId = {this.props.groupId}
graphHttpClient={this.props.graphHttpClient}
/>
</div>;
<p>{headerTitle}</p>
<MembersPicker
people = {this.state.people}
groupId = {this.props.groupId}
graphHttpClient={this.props.graphHttpClient}
/>
</div>;
}
}