Merge pull request #1 from pnp/master

Pull latest master
This commit is contained in:
Nick Brown 2021-04-22 14:48:48 +01:00 committed by GitHub
commit 1e5d75d7c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 39371 additions and 67 deletions

View File

@ -467,4 +467,4 @@ With the following template:
Note that, in the example above, the `person-query` attribute is still bound using the Handlebars syntax `person-query="{{MyPersonField.personValue.email}}"`, whereas the MGT template uses `[[ person.image ]]` and `[[ person.displayName ]]`.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-web parts/samples/react-content-query-online" />
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-content-query-online" />

View File

@ -43,6 +43,7 @@ Version|Date|Comments
1.1|February 24, 2021|Added support for large lists
1.2|March 01, 2021|Fixed search issue for number field
1.3|March 31,2021| Changed UI as per SharePoint list, Set themeing as per current SharePoint site theme, Created custom pagination by using reusable controls, Added features to export CSV based on the filter if the filter is available, Added hyperlink feature for image and link column in export to pdf and also set alternative row formatting in generated pdf as per property pane configuration odd/even row color, fixed object issue (for people/hyperlink, etc) in export to CSV.
1.4|April 10, 2021|Added feature to show profile picture in user column and shows display name of user field in export to CSV and PDF.
## Disclaimer

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -9,7 +9,7 @@
"This web part provides easy way to render SharePoint custom list in data table view with all the necessary features."
],
"created": "2021-03-01",
"modified": "2021-03-01",
"modified": "2021-04-10",
"products": [
"SharePoint",
"Office"
@ -49,4 +49,4 @@
}
]
}
]
]

View File

@ -1,14 +1,16 @@
export function csvCellFormatter(value: any, type: string) {
if (!value) {
return value;
}
switch (type) {
case 'SP.FieldUrl':
value = value.props.children;
break;
default:
break;
}
if (!value) {
return value;
}
switch (type) {
case 'SP.FieldUrl':
value = value.props.children;
break;
case 'SP.FieldUser':
value = value.props.displayName;
default:
break;
}
return value;
}

View File

@ -1,15 +1,17 @@
export function pdfCellFormatter(value: any, type: string) {
if (!value) {
return value;
}
switch (type) {
case 'SP.FieldUrl':
let { children: text, href: link } = value.props;
value = { text, link, color: 'blue' };
break;
default:
break;
}
if (!value) {
return value;
}
switch (type) {
case 'SP.FieldUrl':
let { children: text, href: link } = value.props;
value = { text, link, color: 'blue' };
break;
case 'SP.FieldUser':
value = value.props.displayName;
default:
break;
}
return value;
}

View File

@ -0,0 +1,29 @@
import * as React from 'react';
import { Persona, PersonaSize } from 'office-ui-fabric-react/lib/Persona';
interface IProfilePicProps {
loginName: string;
displayName: string;
getUserProfileUrl: () => Promise<string>;
}
export function RenderProfilePicture(props: IProfilePicProps) {
const [profileUrl, setProfileUrl] = React.useState<string>();
let { displayName, getUserProfileUrl } = props;
React.useEffect(() => {
getUserProfileUrl().then(url => {
setProfileUrl(url);
});
}, [props])
return (
<Persona
imageUrl={profileUrl}
text={displayName}
size={PersonaSize.size32}
imageAlt={displayName}
styles={{ primaryText: { fontSize: '12px' } }}
/>);
}

View File

@ -17,7 +17,7 @@ export class SPService {
for (var i = 0; i < selectedFields.length; i++) {
switch (selectedFields[i].fieldType) {
case 'SP.FieldUser':
selectQuery.push(`${selectedFields[i].key}/Title,${selectedFields[i].key}/Name`);
selectQuery.push(`${selectedFields[i].key}/Title,${selectedFields[i].key}/EMail,${selectedFields[i].key}/Name`);
expandQuery.push(selectedFields[i].key);
break;
case 'SP.FieldLookup':
@ -63,9 +63,10 @@ export class SPService {
}
}
public async getUserProfileUrl(loginName: string, propertyName: string) {
public async getUserProfileUrl(loginName: string) {
try {
const profileUrl = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName);
const properties = await sp.profiles.getPropertiesFor(loginName);
const profileUrl = properties['PictureUrl'];
return profileUrl;
}
catch (err) {

View File

@ -17,7 +17,7 @@ import { DetailsList, DetailsListLayoutMode, DetailsRow, IDetailsRowStyles, IDet
import { pdfCellFormatter } from '../../../shared/common/ExportListItemsToPDF/ExportListItemsToPDFFormatter';
import { csvCellFormatter } from '../../../shared/common/ExportListItemsToCSV/ExportListItemsToCSVFormatter';
import { IPropertyPaneDropdownOption } from '@microsoft/sp-property-pane';
import { RenderProfilePicture } from '../../../shared/common/RenderProfilePicture/RenderProfilePicture';
export default class ReactDatatable extends React.Component<IReactDatatableProps, IReactDatatableState> {
@ -45,6 +45,10 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
this.getSelectedListItems();
}
private getUserProfileUrl = (loginName: string) => {
return this._services.getUserProfileUrl(loginName)
}
public componentDidUpdate(prevProps: IReactDatatableProps) {
if (prevProps.list !== this.props.list) {
this.props.onChangeProperty("list");
@ -82,7 +86,7 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
private _onConfigure() {
this.props.context.propertyPane.open();
}
public formatColumnValue(value: any, type: string) {
if (!value) {
@ -102,7 +106,9 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
value = value['Title'];
break;
case 'SP.FieldUser':
value = value['Title'];
let loginName = value['Name'];
let userName = value['Title'];
value = <RenderProfilePicture loginName={loginName} displayName={userName} getUserProfileUrl={() => this.getUserProfileUrl(loginName)} ></RenderProfilePicture>;
break;
case 'SP.FieldMultiLineText':
value = <div dangerouslySetInnerHTML={{ __html: value }}></div>;
@ -124,6 +130,7 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
}
return value;
}
public formatValueForExportingData(value: any, type?: string) {
if (!value) {
return value;
@ -144,17 +151,17 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
return value;
}
private exportDataFormatter(fields: Array<IPropertyPaneDropdownOption & { fieldType: string }>, listItems: any[], cellFormatterFn: (value: any, type: string) => any){
private exportDataFormatter(fields: Array<IPropertyPaneDropdownOption & { fieldType: string }>, listItems: any[], cellFormatterFn: (value: any, type: string) => any) {
return listItems && listItems.map(item => ({
...fields.reduce((ob, f) => {
ob[f.text] = item[f.key] ? cellFormatterFn(item[f.key], f.fieldType) : '-';
return ob;
}, {})
}));
...fields.reduce((ob, f) => {
ob[f.text] = item[f.key] ? cellFormatterFn(item[f.key], f.fieldType) : '-';
return ob;
}, {})
}));
}
private handlePaginationChange(pageNo: number, pageSize: number) {
this.setState({ page: pageNo, rowsPerPage: pageSize });
private handlePaginationChange(pageNo: number, rowsPerPage: number) {
this.setState({ page: pageNo, rowsPerPage: rowsPerPage });
}
public handleSearch(event: React.ChangeEvent<HTMLInputElement>) {
@ -257,8 +264,8 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
columnHeader={columns.map(c => c.name)}
listName={list}
description={title}
dataSource={()=> this.exportDataFormatter(fields, filteredItems, csvCellFormatter)}
/> : <></>}
dataSource={() => this.exportDataFormatter(fields, filteredItems, csvCellFormatter)}
/> : <></>}
{enableDownloadAsPdf
? <ExportListItemsToPDF
listName={list}
@ -266,7 +273,7 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
columns={columns.map(c => c.name)}
oddRowColor={oddRowColor}
evenRowColor={evenRowColor}
dataSource={()=> this.exportDataFormatter(fields, filteredItems, pdfCellFormatter)} />
dataSource={() => this.exportDataFormatter(fields, filteredItems, pdfCellFormatter)} />
: <></>}
</Grid>
<Grid container justify='flex-end' xs={6}>

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

View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.11.0",
"libraryName": "react-graph-mgt-client",
"libraryId": "8548b10e-1f21-4797-ae74-279081454b9f",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,111 @@
---
page_type: sample
products:
- office-sp
- sharepoint
- microsoft-graph
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
- Microsoft Graph
platforms:
- React
createdDate: 2021-04-18T19:43:46.356Z
---
# Graph MGT Client
## Summary
This is a sample web part developed using React Framework that showcases how to use the latest `microsoft-graph-client` in order to do advanced configuration of the Microsoft Graph client. This enables scenarios like throttling management, Chaos management and a lot more!
![Demo of the Graph Client using the MGT providers](./assets/DemoGraphClient.gif)
## Compatibility
![SPFx 1.11](https://img.shields.io/badge/SPFx-1.11.0-green.svg)
![Node.js LTS 10.x](https://img.shields.io/badge/Node.js-LTS%2010.x-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench")
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
In the `package-solution.json` file, ensure that the scopes are available for the calls you want to do on Microsoft Graph.
```json
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Calendars.Read"
}
],
```
## Solution
Solution|Author(s)
--------|---------
react-graph-mgt-client | [Sébastien Levert](https://www.linkedin.com/in/sebastienlevert), Microsoft ([@sebastienlevert](https://twitter.com/sebastienlevert))
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 18, 2021|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp bundle**
- **gulp package-solution**
- Deploy the generated *.sppkg to your App Catalog
- Approve the Microsoft Graph scopes
- in the command-line run:
- **gulp serve**
## Features
This sample illustrates how to leverage the Microsoft Graph Toolkit providers to extend the experience of using the `microsoft-graph-client` in SharePoint Framework and overcomes some of its current limitations.
This sample illustrates the following concepts:
- Adding MGT providers to an existing SPFx solution
- Replace Microsoft Graph calls built using the integrated Microsoft Graph client
- Use the latest and greatest Microsoft Graph client in order to have more power over your calls
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-graph-mgt-client" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,52 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-graph-mgt-client",
"source": "pnp",
"title": "Graph MGT Client",
"shortDescription": "This is a sample web part developed using React Framework that showcases how to use the latest `microsoft-graph-client` in order to do advanced configuration of the Microsoft Graph client. ",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/master/samples/react-graph-mgt-client",
"longDescription": [
"This is a sample web part developed using React Framework that showcases how to use the latest `microsoft-graph-client` in order to do advanced configuration of the Microsoft Graph client.",
"This enables scenarios like throttling management, Chaos management and a lot more!"
],
"created": "2021-04-18",
"modified": "2021-04-18",
"products": [
"SharePoint",
"Office"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.11.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/master/samples/react-graph-mgt-client/assets/DemoGraphClient.gif",
"alt": "Web Part in action"
}
],
"authors": [
{
"gitHubAccount": "sebastienlevert",
"pictureUrl": "https://github.com/sebastienlevert.png",
"name": "Sébastien Levert",
"twitter": "sebastienlevert"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"graph-latest-client-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/graphClient/GraphClientWebPart.js",
"manifest": "./src/webparts/graphClient/GraphClientWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"GraphClientWebPartStrings": "lib/webparts/graphClient/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-graph-mgt-client",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,31 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-graph-mgt-client-client-side-solution",
"id": "8548b10e-1f21-4797-ae74-279081454b9f",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Calendars.Read"
}
],
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
}
},
"paths": {
"zippedPackage": "solution/react-graph-mgt-client.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "react-graph-mgt-client",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/mgt-element": "^2.1.0",
"@microsoft/mgt-sharepoint-provider": "^2.1.0",
"@microsoft/microsoft-graph-client": "^2.2.1",
"@microsoft/sp-core-library": "1.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"@monaco-editor/react": "^4.1.1",
"office-ui-fabric-react": "6.214.0",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/rush-stack-compiler-3.7": "^0.6.43",
"@microsoft/sp-build-web": "1.11.0",
"@microsoft/sp-module-interfaces": "1.11.0",
"@microsoft/sp-tslint-rules": "1.11.0",
"@microsoft/sp-webpart-workbench": "1.11.0",
"@types/chai": "3.4.34",
"@types/es6-promise": "0.0.33",
"@types/mocha": "2.2.38",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"typescript": "^3.7.7"
}
}

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,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "aaa33b68-65d0-4403-bbe4-1cb21cc1936b",
"alias": "GraphClientWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "GraphClient" },
"description": { "default": "GraphClient description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "GraphClient"
}
}]
}

View File

@ -0,0 +1,53 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import GraphClient from './components/GraphClient';
import { IGraphClientProps } from './components/IGraphClientProps';
// Importing MGT in the Web Part
import { SharePointProvider } from '@microsoft/mgt-sharepoint-provider';
import { Providers } from '@microsoft/mgt-element';
export interface IGraphLatestClientWebPartProps {
description: string;
}
export default class GraphLatestClientWebPart extends BaseClientSideWebPart<IGraphLatestClientWebPartProps> {
public render(): void {
const element: React.ReactElement<IGraphClientProps> = React.createElement(
GraphClient,
{
description: this.properties.description
}
);
ReactDom.render(element, this.domElement);
}
/**
* Assign a new SharePoint provider to the MGT providers
*/
protected async onInit() {
Providers.globalProvider = new SharePointProvider(this.context);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: []
};
}
}

View File

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

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import styles from './GraphClient.module.scss';
import { IGraphClientProps } from './IGraphClientProps';
import { Providers } from '@microsoft/mgt-element';
import { RetryHandlerOptions } from '@microsoft/microsoft-graph-client';
import Editor from '@monaco-editor/react';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
export interface IGraphClientState {
response: any;
apiUrl: string;
loading: boolean;
}
export default class GraphClient extends React.Component<IGraphClientProps, IGraphClientState> {
constructor(props: IGraphClientProps) {
super(props);
this.state = {
response: null,
apiUrl: '',
loading: false
};
}
public render(): React.ReactElement<IGraphClientProps> {
return (
<div className={ styles.graphLatestClient }>
<div className={styles.row}>
<div className={styles.col8}>
<TextField
placeholder="Specify your query"
value={this.state.apiUrl}
onChanged={this._apiUrlChanged}
onKeyUp={(e: React.KeyboardEvent<any>) => e.key === 'Enter' && this._runQuery()}
/>
</div>
<div className={styles.col4}>
<DefaultButton
primary={true}
onClick={this._runQuery}
style={{ width: '100%' }}
disabled={this.state.loading}>
Run query
{this.state.loading && (
<>
<Spinner className={styles.spinner} size={SpinnerSize.xSmall} />
</>
)}
</DefaultButton>
</div>
</div>
<Editor height="600px" theme="vs-dark" defaultLanguage="json" value={this.state.response ? JSON.stringify(this.state.response, null, 2) : ''} />
</div>
);
}
/**
* Event handler for api URL change
*/
private _apiUrlChanged = (val: string) => {
this.setState({
apiUrl: val,
});
}
private _runQuery = async () => {
this.setState({
response: null,
loading: true
});
const data = await Providers.globalProvider.graph.api("/me/events").middlewareOptions([new RetryHandlerOptions(5, 10)]).get();
if(data && data.value && data.value.length > 0) {
this.setState({
response: data,
loading: false
});
}
}
}

View File

@ -0,0 +1,3 @@
export interface IGraphClientProps {
description: string;
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IGraphClientWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'GraphClientWebPartStrings' {
const strings: IGraphClientWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.11.0",
"libraryName": "react-onedrive-finder",
"libraryId": "63bcee97-4373-4a30-9c62-a1369b4d08ba",
"packageManager": "npm",
"isDomainIsolated": true,
"componentType": "webpart"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,57 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-onedrive-finder",
"source": "pnp",
"title": "OneDrive Finder",
"shortDescription": "This sample access drives from OneDrive and navigate between his content using Graph OneDrive and Site API and Microsoft Graph Toolkit React controls with the addition of new FileList control.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/master/samples/react-onedrive-finder",
"longDescription": [
"This sample access drives from OneDrive and navigate between his content using Graph OneDrive and Site API and Microsoft Graph Toolkit React controls with the addition of new FileList control.",
"This new control provides the ability to retrieve the Drive Library with associated Files and folders making easier to develop and navigate between tenant content and access to file. "
],
"created": "2021-04-16",
"modified": "2021-04-16",
"products": [
"SharePoint",
"Office"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.11.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/master/samples/react-onedrive-finder/Assets/OneDrivefinderSample1.PNG",
"alt": "Web Part in action"
},
{
"type": "image",
"order": 101,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/master/samples/react-onedrive-finder/Assets/OneDrivefinderSample2.PNG",
"alt": "Web Part in action"
}
],
"authors": [
{
"gitHubAccount": "aaclage",
"pictureUrl": "https://github.com/aaclage.png",
"name": "André Lage"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,84 @@
# OneDrive finder
## Summary
This sample access drives from OneDrive and navigate between his content using **Graph OneDrive and Site API and [Microsoft Graph Toolkit](https://github.com/microsoftgraph/microsoft-graph-toolkit) react controls "@microsoft/mgt-react**" with the addition of new control **[FileList](https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/f8b8aa81d00bf426b94ee5016d511bc78b36e152/stories/components/fileList.stories.js#L136) "still preview version"** . This new control provides the ability to retrieve the Drive Library with associated Files and folders making easier to develop and navigate between tenant content and access to file.
**[FileList](https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/f8b8aa81d00bf426b94ee5016d511bc78b36e152/stories/components/fileList.stories.js#L136)** control allow to load files base on graph querys or parameters ids.
### Retrieve Sites with drives associate
![Demo](./Assets/OneDrivefinderSample1.PNG)
### Navigate between folders and Breadcrumb
![Demo](./Assets/OneDrivefinderSample2.PNG)
## Compatibility
![SPFx 1.11](https://img.shields.io/badge/SPFx-1.11.0-green.svg)
![Node.js LTS 10.x](https://img.shields.io/badge/Node.js-LTS%2010.x-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench")
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
react-onedrive-finder | [André Lage](http://aaclage.blogspot.com) ([@aaclage](https://twitter.com/aaclage))
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 16, 2021|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp serve**
- **gulp bundle --ship**
- **gulp package-solution --ship**
- Add to AppCatalog and deploy
## Grant the service principal permission to the Microsoft Graph API
Once installed, the solution will request the required permissions via the **Office 365 admin portal > SharePoint > Advanced > API access**.
If you prefer to approve the permissions in advance, for example when testing the solution in the Workbench page without installing it, you can do so using the [CLI for Microsoft 365](https://pnp.github.io/cli-microsoft365/):
```bash
o365 spo login https://contoso-admin.sharepoint.com
o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Files.Read'
o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Files.Read.All'
o365 spo serviceprincipal grant add --resource 'Microsoft Graph' --scope 'Sites.Read.All'
```
## Features
Description of the extension that expands upon high-level summary above.
This extension illustrates the following concepts:
- Easy to navigate between shared Drives using **Graph API and Breadcrumb**
- **[FileList](https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/f8b8aa81d00bf426b94ee5016d511bc78b36e152/stories/components/fileList.stories.js#L136)** control allow to load documents base on graph querys or parameters ids.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-onedrive-finder" />

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"one-drive-finder-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/oneDriveFinder/OneDriveFinderWebPart.js",
"manifest": "./src/webparts/oneDriveFinder/OneDriveFinderWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"OneDriveFinderWebPartStrings": "lib/webparts/oneDriveFinder/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-onedrive-finder",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,31 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-onedrive-finder-client-side-solution",
"id": "63bcee97-4373-4a30-9c62-a1369b4d08ba",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
},
"webApiPermissionRequests": [{
"resource": "Microsoft Graph",
"scope": "Files.Read"
}, {
"resource": "Microsoft Graph",
"scope": "Files.Read.All"
}, {
"resource": "Microsoft Graph",
"scope": "Sites.Read.All"
}]
},
"paths": {
"zippedPackage": "solution/react-onedrive-finder.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "react-onedrive-finder",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/mgt": "^2.2.0-next.file.0e80deb",
"@microsoft/mgt-react": "^2.2.0-next.file.0e80deb",
"@microsoft/sp-core-library": "1.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"office-ui-fabric-react": "6.214.0",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/rush-stack-compiler-3.7": "^0.6.43",
"@microsoft/sp-build-web": "1.11.0",
"@microsoft/sp-module-interfaces": "1.11.0",
"@microsoft/sp-tslint-rules": "1.11.0",
"@microsoft/sp-webpart-workbench": "1.11.0",
"@types/chai": "3.4.34",
"@types/es6-promise": "0.0.33",
"@types/mocha": "2.2.38",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

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,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "88e1b1fd-d39d-4194-966c-9bc1c307bcdf",
"alias": "OneDriveFinderWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "OneDrive finder" },
"description": { "default": "OneDrive finder description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "OneDrive finder"
}
}]
}

View File

@ -0,0 +1,65 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { Providers, SharePointProvider } from '@microsoft/mgt';
import * as strings from 'OneDriveFinderWebPartStrings';
import OneDriveFinder from './components/OneDriveFinder';
import { IOneDriveFinderProps } from './components/IOneDriveFinderProps';
export interface IOneDriveFinderWebPartProps {
description: string;
}
export default class OneDriveFinderWebPart extends BaseClientSideWebPart<IOneDriveFinderWebPartProps> {
protected onInit() {
Providers.globalProvider = new SharePointProvider(this.context);
return super.onInit();
}
public render(): void {
const element: React.ReactElement<IOneDriveFinderProps> = React.createElement(
OneDriveFinder,
{
description: this.properties.description,
context: this.context
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,6 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IOneDriveFinderProps {
description: string;
context: WebPartContext;
}

View File

@ -0,0 +1,11 @@
import { IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcrumb';
import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
export interface IOneDriveFinderState {
breadcrumbItem: IBreadcrumbItem[];
pageSize: number;
siteID:string;
itemID:string;
siteItems: IDropdownOption[];
}

View File

@ -0,0 +1,17 @@
.some-page-wrapper {
margin: 5px;
}
.row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
}
.column {
display: flex;
flex-direction: column;
flex-basis: 100%;
flex: 1;
}

View File

@ -0,0 +1,364 @@
import * as React from 'react';
import styles from './OneDriveFinder.module.scss';
import { IOneDriveFinderProps } from './IOneDriveFinderProps';
import { IOneDriveFinderState } from './IOneDriveFinderState';
import { FileList } from '@microsoft/mgt-react';
import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcrumb';
import { Dropdown, IDropdownOption, IDropdownProps } from 'office-ui-fabric-react/lib/Dropdown';
import { AadHttpClient } from "@microsoft/sp-http";
import { ITheme, mergeStyleSets, getTheme } from 'office-ui-fabric-react/lib/Styling';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
const theme: ITheme = getTheme();
const { palette, fonts } = theme;
const iconStyles = { marginRight: '8px' };
const classNames = mergeStyleSets({
itemIndex: {
fontSize: fonts.small.fontSize,
color: palette.neutralTertiary,
marginBottom: 5,
},
});
const dropdownStyles = {
dropdown: { width: 300 },
root: {
}
};
const dropdownFilterStyles = {
dropdown: {
width: 130
},
label: {
},
root: {
paddingLeft: 90
}
};
const onRenderCaretDown = (): JSX.Element => {
return <Icon iconName="PageRight" />;
};
/**
*
* @param option
* Render dropdown list of sites.
*/
const onRenderOption = (option: IDropdownOption): JSX.Element => {
return (
<div>
<div>
{option.data && option.data.icon && (
<Icon style={iconStyles} iconName={option.data.icon} aria-hidden="true" title={option.data.icon} />
)}
<span>{option.text}</span>
</div>
<div className={classNames.itemIndex}>{option.data.webUrl}</div>
</div>
);
};
/**
* Render Selected dropdown
*/
const onRenderTitle = (options: IDropdownOption[]): JSX.Element => {
const option = options[0];
return (
<div>
<div>
{option.data && option.data.icon && (
<Icon style={iconStyles} iconName={option.data.icon} aria-hidden="true" title={option.data.icon} />
)}
<span>{option.text}</span>
</div>
<div className={classNames.itemIndex}>{option.data.webUrl}</div>
</div>
);
};
const onRenderPlaceholder = (props: IDropdownProps): JSX.Element => {
return (
<div className="dropdownExample-placeholder">
<span>{props.placeholder}</span>
</div>
);
};
export default class OneDriveFinder extends React.Component<IOneDriveFinderProps, IOneDriveFinderState> {
public _domain: string;
public _itemID: string;
public _siteID: string;
public _pageSize: number;
public _breadcrumbItem: IBreadcrumbItem[] = [];
constructor(props: IOneDriveFinderProps, state: IOneDriveFinderState) {
super(props);
// Initialize the state of the component
this.state = {
breadcrumbItem: [],
pageSize: 50,
siteID: "",
siteItems: [],
itemID: ""
};
this.getDomainData();
}
public render(): React.ReactElement<IOneDriveFinderProps> {
const { siteID, itemID, pageSize, breadcrumbItem, siteItems, } = this.state;
this._itemID = itemID;
this._siteID = siteID;
this._breadcrumbItem = breadcrumbItem;
return (
<div>
<div className={styles['some-page-wrapper']}>
<div className={styles.row}>
<div className={styles.column}>
<Dropdown
placeholder="Select an Site"
label="List of Drives"
ariaLabel="Custom dropdown example"
onRenderPlaceholder={onRenderPlaceholder}
onRenderOption={onRenderOption}
onRenderTitle={onRenderTitle}
onRenderCaretDown={onRenderCaretDown}
styles={dropdownStyles}
options={siteItems}
onChange={(e, selectedOption) => {
if (selectedOption.data.root == undefined) {
this._siteID = selectedOption.key.toString();
this.setState({
siteID: this._siteID,
breadcrumbItem: this._breadcrumbItem
});
} else {
this._siteID = "";
this.setState({
siteID: this._siteID,
itemID: "",
breadcrumbItem: this._breadcrumbItem
});
}
this.getDrives(selectedOption);
}}
/>
</div>
<div className={styles.column}>
<Dropdown
placeholder="50 Items"
label="Filter Items"
defaultValue={'50 Items'}
styles={dropdownFilterStyles}
options={[
{ key: 5, text: '5 Items' },
{ key: 10, text: '10 Items' },
{ key: 50, text: '50 Items' },
{ key: 100, text: '100 Items' },
{ key: 500, text: '500 Items' },
{ key: 1000, text: '1000 Items' },
]}
onChange={(e, selectedOption) => {
let _pageSize: number = +selectedOption.key;
this._pageSize = _pageSize;
this.setState({
pageSize: _pageSize
});
}}
/>
</div>
</div>
</div>
<div>
<Breadcrumb
items={this._breadcrumbItem}
maxDisplayedItems={10}
ariaLabel="Breadcrumb with items rendered as buttons"
overflowAriaLabel="More links"
/>
{(this.state.itemID != "" || this.state.itemID != "") &&
<FileList
pageSize={pageSize}
siteId={this._siteID}
itemId={this._itemID}
itemClick={this.manageFolder}
></FileList>
}
</div>
</div>
);
}
/**
*
* @param e
* Capture file or folder and manages breadcrumb
*/
private manageFolder = (e: any) => {
if (e.detail.folder != undefined) {
this._breadcrumbItem.push({
text: e.detail.name,
key: e.detail.id,
onClick: (event, item) => {
let _cleanBreadcrumbItems: IBreadcrumbItem[] = [];
var i = 0;
this._breadcrumbItem.some((value) => {
if (i == 0) {
_cleanBreadcrumbItems.push(value);
if (value.key === item.key) {
i++;
}
}
});
this._itemID = e.detail.id;
this.setState({
itemID: item.key,
breadcrumbItem: _cleanBreadcrumbItems
});
}
});
this.setState({
itemID: e.detail.id,
breadcrumbItem: this._breadcrumbItem
});
} else {
window.open(e.detail.webUrl, '_blank');
}
}
private getRootDriveFolderID = async (siteID) => {
let graphData: any = await this.getGraphContent("https://graph.microsoft.com/v1.0/sites/" + siteID + "/drive/root://:/?$select=id");
return graphData.id;
}
private getOneDriveRootFolderID = async (key) => {
let graphData: any = await this.getGraphContent("https://graph.microsoft.com/v1.0/me/drive/items/" + key + "?$select=id");
return graphData.id;
}
/**
* Retrieves Folder item ID from Site or OneDrive
*/
private getDrives = async (selectedOption: IDropdownOption) => {
let itemID: any;
if (selectedOption.data.root != undefined) {
itemID = await this.getOneDriveRootFolderID(selectedOption.key)
} else {
itemID = await this.getRootDriveFolderID(selectedOption.key)
}
this._breadcrumbItem = [];
this._breadcrumbItem.push({
text: selectedOption.text,
key: itemID,
onClick: (e, item) => {
let _cleanBreadcrumbItems: IBreadcrumbItem[] = [];
var i = 0;
this._breadcrumbItem.some((value) => {
if (i == 0) {
_cleanBreadcrumbItems.push(value);
if (value.key === item.key) {
i++;
}
}
});
this._itemID = itemID;
this.setState({
itemID: itemID,
breadcrumbItem: _cleanBreadcrumbItems
});
}
});
this.setState(
{
breadcrumbItem: this._breadcrumbItem,
itemID: itemID
});
}
/**
* Retrieve domain name
*/
private getDomainData = async () => {
let graphData: any = await this.getGraphContent("https://graph.microsoft.com/v1.0/sites/root?$select=siteCollection");
this.getSiteData(graphData);
}
/**
*
* @param DomainData
* Retrieves sites from domain
*/
private getSiteData = async (DomainData) => {
let MyDriveData: any = await this.getGraphContent("https://graph.microsoft.com/v1.0/me/drive/root/?$Select=id,name,displayName,webUrl");
let graphData: any = await this.getGraphContent("https://graph.microsoft.com/v1.0/sites?search=" + escape(DomainData.siteCollection.hostname.split(".")[0]) + ".sharepoint&$Select=id,name,displayName,webUrl");
var sharedSitesOptions: Array<IDropdownOption> = new Array<IDropdownOption>();
// Map the JSON response to the output array
graphData.value.map((item: any) => {
sharedSitesOptions.push({
key: item.id,
text: item.displayName,
data: { icon: 'Globe', webUrl: item.webUrl.split("sharepoint.com")[1] }
});
});
//Sort by Web url
sharedSitesOptions = sharedSitesOptions.sort((Option1, Option2) => {
if (Option1.data.webUrl > Option2.data.webUrl) {
return 1;
}
if (Option1.data.webUrl < Option2.data.webUrl) {
return -1;
}
return 0;
});
//OneDrive Folder is added as First on List
sharedSitesOptions.unshift({
key: MyDriveData.id,
text: MyDriveData.name,
data: { icon: 'OneDriveFolder16', webUrl: MyDriveData.webUrl.split("sharepoint.com")[1], root: true }
});
// Update the component state accordingly to the result
this.setState(
{
siteItems: sharedSitesOptions,
}
);
}
/**
* Method to Connect Graph
*/
private getGraphContent = (graphQuery: string) => {
// Using Graph here, but any 1st or 3rd party REST API that requires Azure AD auth can be used here.
return new Promise<any>((resolve, reject) => {
this.props.context.aadHttpClientFactory
.getClient("https://graph.microsoft.com")
.then((client: AadHttpClient) => {
// Querys to Graph base on url
return client
.get(
`${graphQuery}`,
AadHttpClient.configurations.v1
);
})
.then(response => {
return response.json();
})
.then(json => {
resolve(json);
})
.catch(error => {
console.error(error);
reject(error);
});
});
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IOneDriveFinderWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'OneDriveFinderWebPartStrings' {
const strings: IOneDriveFinderWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

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

View File

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

View File

@ -147,8 +147,14 @@ By default you have to install this web part per site collection where you want
In order to make it available to absolutely all sites you need apply the _Deploy to non-script sites / modern team site_ change as well.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.10.0-green.svg)
## Compatibility
![SPFx 1.10](https://img.shields.io/badge/SPFx-1.10.0-green.svg)
![Node.js LTS 10.x](https://img.shields.io/badge/Node.js-LTS%2010.x%20%7C%20LTS%208.x-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
![Workbench Local | Hosted](https://img.shields.io/badge/Workbench-Local%20%7C%20Hosted-green.svg)
## Applies to

View File

@ -88,4 +88,4 @@ Version|Date|Comments
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-web parts/samples/react-content-query-online" />
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-content-query-online" />

View File

@ -31,7 +31,7 @@ This web part shows the current user's colleagues, and allows the user to search
![SPFx 1.11](https://img.shields.io/badge/SPFx-1.11.0-green.svg)
![Node.js LTS 10.x](https://img.shields.io/badge/Node.js-LTS%2010.x-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
![Teams Yes: Designed for Microsoft Teams](https://img.shields.io/badge/Teams-Yes-green.svg "Designed for Microsoft Teams")
![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench")
## Applies to
@ -45,6 +45,8 @@ Solution|Author(s)
--------|---------
react-staffdirectory|Ari Gunawan ([@arigunawan3023](https://twitter.com/arigunawan3023))
react-staffdirectory|João Mendes ([joaojmendes](https://github.com/joaojmendes))
react-staffdirectory|[Tristian O'brien](https://github.com/tristian2)
## Version history
@ -53,6 +55,7 @@ Version|Date|Comments
-------|----|--------
1.0.0|February 16, 2021|Initial release
1.0.1|March 28, 2021|Added missing Graph API Permission (User.Read.All) for getting users information
1.0.2|April 14, 2021|Added About Me and Skills
## Disclaimer

View File

@ -9,7 +9,7 @@
"This web part shows the current user\u0027s colleagues, and allows the user to search AD directory, The user can configure the properties to show when expand the user card."
],
"created": "2021-03-09",
"modified": "2021-03-28",
"modified": "2021-04-14",
"products": [
"SharePoint",
"Office"

View File

@ -3,7 +3,7 @@
"solution": {
"name": "staff-directory-client-side-solution",
"id": "89d7389c-be48-41e9-9f72-5eb9a1099c1f",
"version": "1.0.1.0",
"version": "1.0.2.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,

View File

@ -1,6 +1,6 @@
{
"name": "staff-directory",
"version": "0.0.1",
"version": "1.0.2",
"private": true,
"main": "lib/index.js",
"engines": {

View File

@ -19,7 +19,6 @@ import { IUserExtended } from "../../entites/IUserExtended";
import { IAppContext } from "../../common/IAppContext";
import { IUserCardProps } from "./IUserCardProps";
const teamsDefaultTheme = require("../../common/TeamsDefaultTheme.json");
const teamsDarkTheme = require("../../common/TeamsDarkTheme.json");
const teamsContrastTheme = require("../../common/TeamsContrastTheme.json");
@ -97,6 +96,7 @@ export const UserCard = (props: IUserCardProps) => {
}
};
//tris added onclick event
const _onRenderPrimaryText = (persona: IPersonaProps) => {
return (
<>
@ -108,7 +108,12 @@ export const UserCard = (props: IUserCardProps) => {
root: { justifyContent: "flex-start", width: "100%" },
}}
>
<Text
<Text onClick={
(event) => {
event.preventDefault();
window.open("https://gbr.delve.office.com/?u=" + userData.id + "&v=work", "_blank");
}
}
variant="medium"
block
nowrap
@ -116,8 +121,9 @@ export const UserCard = (props: IUserCardProps) => {
width: "100%",
fontWeight: 600,
padding: 0,
marginBottom: 3,
marginBottom: 3
}}
title={persona.text}
>
{persona.text}
</Text>
@ -130,6 +136,7 @@ export const UserCard = (props: IUserCardProps) => {
>
<div style={{ fontSize: 12 }}>
<ActionButton
styles={{
root: {
height: 21,
@ -184,11 +191,19 @@ export const UserCard = (props: IUserCardProps) => {
);
};
//tris added onclick event
const _onRenderSecondaryText = (persona: IPersonaProps) => {
return (
<>
<Stack verticalAlign="start" tokens={{ childrenGap: 0 }}>
<Text title={persona.secondaryText} variant="medium" block nowrap>
<Text onClick={
(event) => {
event.preventDefault();
window.open("https://gbr.delve.office.com/?u=" + userData.id + "&v=work", "_blank");
}
}
title={persona.secondaryText} variant="medium" block nowrap>
{" "}
{persona.secondaryText}
</Text>
@ -221,11 +236,11 @@ export const UserCard = (props: IUserCardProps) => {
}
text={userData.displayName}
title={userData.displayName}
tertiaryText={userData.mail}
tertiaryText={userData.mail}
secondaryText={userData.jobTitle}
onRenderPrimaryText={_onRenderPrimaryText}
onRenderSecondaryText={_onRenderSecondaryText}
></Persona>
></Persona>
</Stack>
{isDetailsOpen && (
<>
@ -707,8 +722,82 @@ export const UserCard = (props: IUserCardProps) => {
<div className={styleClasses.separator}></div>
</div>
);
break;
}
break;
case "aboutMe":
return (
<div
className={`${styleClasses.styleField}`}
title={userData.aboutMe ? userData.aboutMe.replace(/<[^>]+>/g, '').replace(/&nbsp;/gi,' ') : "Not available"}
>
<Stack
horizontal={true}
verticalAlign="center"
tokens={{ childrenGap: 5 }}
style={{ marginRight: 20, marginLeft: 20 }}
>
<FontIcon
className={styleClasses.styleIconDetails}
iconName="Medal"
/>
<Label className={styleClasses.styleFieldLabel}>
About Me
</Label>
</Stack>
<Text
styles={styleTextField}
variant="medium"
block={true}
nowrap={true}
>
{userData.aboutMe ? userData.aboutMe.replace(/<[^>]+>/g, '').replace(/&nbsp;/gi,' ') : "Not available"}
</Text>
<div className={styleClasses.separator}></div>
</div>
);
break;
case "skills":
return (
<div
className={`${styleClasses.styleField}`}
title={
userData.skills.join(",")
? userData.skills.join(",")
: "Not available"
}
>
<Stack
horizontal={true}
verticalAlign="center"
tokens={{ childrenGap: 5 }}
style={{ marginRight: 20, marginLeft: 20 }}
>
<FontIcon
className={styleClasses.styleIconDetails}
iconName="Education"
/>
<Label className={styleClasses.styleFieldLabel}>
Skills
</Label>
</Stack>
<Text
styles={styleTextField}
variant="medium"
block={true}
nowrap={true}
>
{userData.skills.join(",") ? (
<>
{userData.skills.join(",")}
</>
) : (
"Not available"
)}
</Text>
<div className={styleClasses.separator}></div>
</div>
);
break;
}
})}
</Stack>
</>

View File

@ -15,5 +15,4 @@ export interface IUser {
officeLocation: string;
postalCode: string;
userType: string;
}

View File

@ -0,0 +1,5 @@
export interface IUserBio {
id?: string;
aboutMe: string;
skills: string[];
}

View File

@ -1,6 +1,7 @@
import { IUser } from "./IUser";
import { IUserPresence } from "./IUserPresence";
export interface IUserExtended extends IUser, IUserPresence {
import { IUserBio } from "./IUserBio";
export interface IUserExtended extends IUser, IUserPresence, IUserBio {
count: number;
pictureBase64: string;
}

View File

@ -4,8 +4,10 @@ import "@pnp/graph/users";
import { IUserExtended } from "../entites/IUserExtended";
import { IUser } from "../entites/IUser";
import { IUserPresence } from "../entites/IUserPresence";
import { IUserBio } from "../entites/IUserBio";
import { SPComponentLoader } from "@microsoft/sp-loader";
import { findIndex } from "lodash";
import { findIndex, join, values } from "lodash";
import { DetailsRow } from "office-ui-fabric-react";
/*************************************************************************************/
// Hook to search users
@ -22,6 +24,7 @@ export const useSearchUsers = async (
pageSize?: number
): Promise<{ usersExtended: IUserExtended[]; nextPage: string }> => {
pageSize = pageSize ? pageSize : 5;
const _searchResults: any = await _MSGraphClient
.api('/users?$search="' + searchString + '"')
.version("beta")
@ -40,8 +43,11 @@ export const useSearchUsers = async (
for (const _user of _users) {
const _userPresence = await getUserPresence(_user.id, _MSGraphClient);
const _pictureBase64: string = await getUserPhoto(_user.mail);
const _userBio = await getUserBio(_user.id, _MSGraphClient);
_usersExtended.push({
..._user,
..._userBio,
..._userPresence,
pictureBase64: _pictureBase64,
count: 0,
@ -77,14 +83,18 @@ export const useGetUsersByDepartment = async (
.count(true)
.get();
const _users: IUser[] = _searchResults.value;
let _usersExtended: IUserExtended[] = [];
for (const _user of _users) {
const _userPresence = await getUserPresence(_user.id, _MSGraphClient);
const _pictureBase64: string = await getUserPhoto(_user.mail);
const _userBio = await getUserBio(_user.id, _MSGraphClient);
_usersExtended.push({
..._user,
..._userBio,
..._userPresence,
pictureBase64: _pictureBase64,
count: 0,
@ -119,8 +129,10 @@ export const useGetUsersNextPage = async (
for (const _user of _users) {
const _userPresence = await getUserPresence(_user.id, _MSGraphClient);
const _pictureBase64: string = await getUserPhoto(_user.mail);
const _userBio = await getUserBio(_user.id, _MSGraphClient);
_usersExtended.push({
..._user,
..._userBio,
..._userPresence,
pictureBase64: _pictureBase64,
count: 0,
@ -185,6 +197,21 @@ const getUserPresence = async (
return _presence;
};
//*************************************************************************************//
// function Get Users About Me and skillz
//*************************************************************************************//
const getUserBio = async (
userObjId,
_MSGraphClient
): Promise<IUserBio> => {
let _bio : IUserBio = await _MSGraphClient
.api("/users/{" + userObjId + "}?$select=aboutMe,skills")
.version("beta")
.get();
return _bio;
};
/**
* Gets user photo
* @param userId

View File

@ -18,11 +18,11 @@
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "SPFx Custom Web Parts" },
"title": { "default": "Search Directory" },
"description": { "default": "Search Directory" },
"title": { "default": "UoB Search Directory" },
"description": { "default": "University of Brighton Search Directory" },
"officeFabricIconFontName": "ProfileSearch",
"properties": {
"title": "Search Directory",
"title": "UoB Search Directory",
"maxHeight": 700,
"showBox": true,
"refreshInterval": 3,

View File

@ -42,8 +42,6 @@ export default class StaffDirectoryWebPart extends BaseClientSideWebPart<IStaffD
protected async onInit(): Promise<void> {
this._themeProvider = this.context.serviceScope.consume(
ThemeProvider.serviceKey
);
@ -189,7 +187,6 @@ export default class StaffDirectoryWebPart extends BaseClientSideWebPart<IStaffD
PropertyFieldMultiSelect('userAttributes', {
key: 'userAttributes',
label: strings.UserAttributesLabel,
options: [
{
key: "company",
@ -223,6 +220,14 @@ export default class StaffDirectoryWebPart extends BaseClientSideWebPart<IStaffD
key: "userType",
text: "User Type"
},
{
key: "aboutMe",
text: "About Me"
},
{
key: "skills",
text: "Skills"
},
],
selectedKeys: this.properties.userAttributes
}),

View File

@ -8,8 +8,8 @@
"longDescription": [
"TODO"
],
"created": "2021-04-02",
"modified": "2021-04-02",
"created": "2020-04-27",
"modified": "2021-04-20",
"products": [
"SharePoint",
"Office"