Add sample that shows documents webparts with DetailsList (+ filtering, sorting ) (#334)

* Add new sample: react-documents. Initial commit

* Fix sample description
This commit is contained in:
Dimcho Tsanov 2017-10-25 19:37:54 +03:00 committed by Vesa Juvonen
parent 6c3b2c74ce
commit 6680c156c5
36 changed files with 1928 additions and 0 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

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,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.3.2",
"libraryName": "react-documents",
"libraryId": "aaecff2e-2087-4ade-bcf8-e72f89a1d933",
"environment": "spo"
}
}

View File

@ -0,0 +1,54 @@
# Documents Web Part
## Summary
This sample shows how to build web parts that display documents in accordance with the SharePoint Online modern experience. The code uses Office UI Fabric components on the top of SharePoint framework. The web parts implement filtering and sorting. Two data source approaches are demonstrated: items retrieved from the search index and real-time query to a document library.
![Demo](./assets/Preview.gif)
## 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)
## Prerequisites
- Office 365 subscription with SharePoint Online.
- SharePoint Framework [development environment](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) already set up.
## Solution
Solution|Author(s)
--------|---------
react-documents|Dimcho Tsanov
## Version history
Version|Date|Comments
-------|----|--------
1.0|October 13, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
-Using React for building SharePoint Framework client-side web parts.
-Using Office UI Fabric React styles for building user experience consistent with SharePoint and Office.
-Using the SharePoint rest API for querying document library's files.
-Using the SharePoint rest API for retrieving documents from the search index.
-Passing web part properties to React components.
-Reusing single React component between two web parts.

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

View File

@ -0,0 +1,27 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"search-documents-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/searchDocuments/SearchDocumentsWebPart.js",
"manifest": "./src/webparts/searchDocuments/SearchDocumentsWebPart.manifest.json"
}
]
},
"library-documents-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/libraryDocuments/LibraryDocumentsWebPart.js",
"manifest": "./src/webparts/libraryDocuments/LibraryDocumentsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"SearchDocumentsWebPartStrings": "lib/webparts/searchDocuments/loc/{locale}.js",
"LibraryDocumentsWebPartStrings": "lib/webparts/libraryDocuments/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,11 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-documents-client-side-solution",
"id": "aaecff2e-2087-4ade-bcf8-e72f89a1d933",
"version": "1.0.0.0"
},
"paths": {
"zippedPackage": "solution/react-documents.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.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,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"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-case": true,
"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,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,5 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
//NOTE: the cdnBasePath works with relative path; this is useful when you move the pakedge between different environments (dev, staging, prod)
"cdnBasePath": "/sites/dev03/Style Library/CDN"
}

View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,34 @@
{
"name": "react-documents",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "15.4.2",
"react-dom": "15.4.2",
"@types/react": "15.0.38",
"@types/react-dom": "0.14.18",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-update": "0.14.14",
"@types/react-addons-test-utils": "0.14.15",
"@microsoft/sp-core-library": "~1.3.0",
"@microsoft/sp-webpart-base": "~1.3.0",
"@microsoft/sp-lodash-subset": "~1.3.0",
"@types/webpack-env": ">=1.12.1 <1.14.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.3.0",
"@microsoft/sp-module-interfaces": "~1.3.0",
"@microsoft/sp-webpart-workbench": "~1.3.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
}
}

View File

@ -0,0 +1,14 @@
export interface IDocument {
Id?: number;
Name?: string;
FileRef?: string;
Modified?: any;
ModifiedBy?: any;
FileIcon?: string;
VersionString?: string;
ContentType?: string;
ParentWebUrl?: string;
UniqueId?: string;
}

View File

@ -0,0 +1,277 @@
import { IDocument } from './IObjects';
import { IContextualMenuItem, IColumn } from 'office-ui-fabric-react';
export class Utils {
/**
* Returns the site relative url from an absolute url
*/
public GetRelativePathFromAbsolute(absoluteUrl) {
var serverRelativeUrl =
absoluteUrl.toLowerCase().replace(window.location.protocol.toLowerCase() + "//" + window.location.host.toLowerCase(), "");
return serverRelativeUrl;
}
/**
* Returns the site relative url from an absolute url
*/
public GetFilterValues(column: IColumn, arrayObjects: any[], onFilterClickCallback: (ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem) => void): IContextualMenuItem[] {
let filters: IContextualMenuItem[] = [];
for (let i = 0; i < arrayObjects.length; i++) {
let item = arrayObjects[i];
let value: string = item[column.key];
if (item[column.key]) {
//in case we have specific column, we can add more complex logic
if (column.data == "Taxonomy") {
let columnValue: string = item[column.key];
let valuesAsStrings: string[] = columnValue.split(";");
valuesAsStrings.map((termValue) => {
termValue = termValue.trim();
if (termValue && !this._IsValuePresented(filters, termValue)) {
filters.push(
{
key: termValue,
name: termValue,
data: column.key,
onClick: onFilterClickCallback,
isChecked: i == 0 ? true : false
});
}
});
}
else {
if (!this._IsValuePresented(filters, value)) {
filters.push(
{
key: value,
name: value,
data: column.key,
onClick: onFilterClickCallback,
isChecked: i == 0 ? true : false
});
}
}
}
}
return filters;
}
/**
* Returns sorting menu
*/
public GetSortingMenuItems(column: IColumn, onSortColumn: (column: IColumn, isSortedDescending: boolean) => void): IContextualMenuItem[] {
let menuItems = [];
if (column.data == Number) {
menuItems.push(
{
key: 'smallToLarger',
name: 'Smaller to larger',
canCheck: true,
checked: column.isSorted && !column.isSortedDescending,
onClick: () => onSortColumn(column, false)
},
{
key: 'largerToSmall',
name: 'Larger to smaller',
canCheck: true,
checked: column.isSorted && column.isSortedDescending,
onClick: () => onSortColumn(column, true)
}
);
}
else if (column.data == Date) {
menuItems.push(
{
key: 'oldToNew',
name: 'Older to newer',
canCheck: true,
checked: column.isSorted && !column.isSortedDescending,
onClick: () => onSortColumn(column, false)
},
{
key: 'newToOld',
name: 'Newer to Older',
canCheck: true,
checked: column.isSorted && column.isSortedDescending,
onClick: () => onSortColumn(column, true)
}
);
}
else
//(column.data == String)
// NOTE: in case of 'complex columns like Taxonomy, you need to add more logic'
{
menuItems.push(
{
key: 'aToZ',
name: 'A to Z',
canCheck: true,
checked: column.isSorted && !column.isSortedDescending,
onClick: () => onSortColumn(column, false)
},
{
key: 'zToA',
name: 'Z to A',
canCheck: true,
checked: column.isSorted && column.isSortedDescending,
onClick: () => onSortColumn(column, true)
}
);
}
return menuItems;
}
/**
* Returns image url for the given filename.
* The urls points to https://spoprod-a.akamaihd.net..... !!!
*/
public GetImgUrl(fileName: string): string {
let fileNameItems = fileName.split('.');
let fileExtenstion = fileNameItems[fileNameItems.length - 1];
return this.GetImgUrlByFileExtension(fileExtenstion);
}
/**
* Returns image url for the given extension.
* The urls points to https://spoprod-a.akamaihd.net..... !!!
*/
public GetImgUrlByFileExtension(extension: string): string {
// cuurently in SPFx with React I didn't find different way of getting the image
// feel free to improve this
let imgRoot: string = "https://spoprod-a.akamaihd.net/files/odsp-next-prod_ship-2017-04-21-sts_20170503.001/odsp-media/images/filetypes/16/";
let imgType = "genericfile.png";
imgType = extension + ".png";
switch (extension) {
case "jpg":
case "jpeg":
case "jfif":
case "gif":
case "png":
imgType = "photo.png";
break;
case "folder":
imgType = "folder.svg";
break;
}
return imgRoot + imgType;
}
/**
* Returns formated date string
*/
public GetFormatedDate(dateValue: Date): string {
if (dateValue) {
let date: string = dateValue.toLocaleString();
if (date.indexOf(',') > -1) {
date = date.split(',')[0];
}
return date;
}
return "";
}
/**
* Returns formated date string
*/
public GetFormatedDateString(dateString: string): string {
if (dateString) {
let convertedDate: Date = new Date(dateString);
let date: string = convertedDate.toLocaleString();
if (date.indexOf(',') > -1) {
date = date.split(',')[0];
}
return date;
}
return "";
}
/**
* Returns formated date string
*/
public GetDateOnly(dateString: string): string {
let shortDate = "";
if (dateString) {
let dateItems = dateString.split(" ");
if (dateItems.length > 1) {
shortDate = dateItems[0];
}
}
let convertedDate: Date = new Date(dateString);
return shortDate;
}
/**
* Returns the file name by spliting the file url
*/
public GetFileName(fileAbsoluteUrl: string): string {
if (fileAbsoluteUrl) {
let items = fileAbsoluteUrl.split('/');
return items[items.length - 1];
}
return "";
}
/**
* Gets the FileRef value from the absolute url
*/
public GetFileRef(fileAbsoluteUrl: string): string {
if (fileAbsoluteUrl) {
return fileAbsoluteUrl.replace(window.location.origin, "");
}
return "";
}
/**
* Gets the Content Type value from the value of the search manged property ContentType
*/
public GetContentType(searchValue: string) {
//the string value is in the format:
// "application/vnd.openxmlformats-officedocument.wordprocessingml.document
// <line break>
// Document"
debugger;
searchValue = searchValue.replace(/\r?\n|\r/g, "#");
let result: string = "";
if (searchValue) {
if (searchValue.indexOf("#") > 0) {
result = searchValue.split("#")[searchValue.split("#").length - 1];
}
}
return result;
}
/**
* Opens the document in a new tab. The code use window.open
*/
public OpenDocument(docItem: IDocument, thisContext: any, openPDFInClient: boolean): void {
let newTabObject: any = null;
try {
let documentWebUrl: string = "";
if (docItem.FileRef.toLowerCase().indexOf(".pdf") > 0) {
documentWebUrl = window.location.origin + docItem.FileRef + "?web=1";
}
else {
documentWebUrl = docItem.ParentWebUrl + "/_layouts/WopiFrame.aspx"
+ "?sourcedoc=" + encodeURIComponent("{" + docItem.UniqueId + "}") + "&action=default";
}
newTabObject = window.open(documentWebUrl);
}
catch (ex) {
//optionaly, we can notify the user;
// cuurently - do nothing
}
}
/**
* Helper method that check if a value is in the IContextualMenuItem[]
*/
private _IsValuePresented(currentValues: IContextualMenuItem[], newValue: string): boolean {
for (let i = 0; i < currentValues.length; i++) {
if (currentValues[i].key == newValue) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,115 @@
.documents {
.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 {
padding: 20px;
}
.listItem {
max-width: 715px;
margin: 5px auto 5px auto;
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: "[theme:themePrimary, default:#0078d7]";
border-color: "[theme:themePrimary, default:#0078d7]";
color: #ffffff;
// 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: 14px;
font-weight: 400;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: 600;
font-size: 14px;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}
.notification{
text-align: center;
padding: 50px;
}
.notificationIcon{
font-size: 20px;
margin-right: 10px;
}
.notificationHeader
{
font-size: 20px;
}
.notificationDescription
{
font-size: 18px;
margin-top: 20px;
}
.loadingWrapper
{
padding-top: 80px;
padding-bottom: 80px;
}
.topBar
{
border-top: 1px solid #eaeaea;
height: 40px;
background-color: #f4f4f4;
}
.topBarText
{
position: relative;
top:7px;
left:20px;
}
.topBarFilters
{
text-align: right;
position: relative;
bottom: 8px;
right: 30px;
font-size: 16px;
}
.calloutHeader {
padding: 18px 24px 12px;
}
.resetFilter
{
margin-right: 20px;
cursor: pointer;
}
.docsWrapper{
overflow-y: auto;
}
.iframeWrapper
{
width: 95%;
height: 0px;
}

View File

@ -0,0 +1,505 @@
import * as React from 'react';
import styles from './Documents.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';
import { DisplayMode } from '@microsoft/sp-core-library';
import {
Link, MarqueeSelection, DetailsList, Selection, Image, ImageFit,
SelectionMode, Spinner, SpinnerSize, Fabric, ColumnActionsMode, IColumn, CheckboxVisibility,
Callout, Panel, PanelType, IContextualMenuItem, autobind, ContextualMenu, IContextualMenuProps, DirectionalHint,
css
} from 'office-ui-fabric-react';
import { IDocumentsProps } from './IDocumentsProps';
import IDocumentsState from './IDocumentsState';
import * as _ from "lodash";
import MockupDataProvider from '../../../dataproviders/MockupDataProvider';
import IDataProvider from '../../../dataproviders/IDataProvider';
import { IDocument } from '../../../common/IObjects';
import { Utils } from '../../../common/Utils';
export default class LibraryDocuments extends React.Component<IDocumentsProps, IDocumentsState> {
private _selection: Selection;
private _isConfigurationValid: boolean = true;
constructor(props: IDocumentsProps) {
super(props);
this._isConfigurationValid = false;
if (props.dataProvider) {
this._isConfigurationValid = props.dataProvider.validateSettings();
}
this.state = {
allDocuments: [],
displayedDocuments: [],
isLoading: true,
contextualMenuProps: null,
columns: this._setupColumns()
};
this._renderItemColumn = this._renderItemColumn.bind(this);
this._onResetFiltersClicked = this._onResetFiltersClicked.bind(this);
this._onContextualMenuDismissed = this._onContextualMenuDismissed.bind(this);
this._getContextualMenuProps = this._getContextualMenuProps.bind(this);
}
public render(): React.ReactElement<IDocumentsProps> {
let { contextualMenuProps } = this.state;
if (!this._isConfigurationValid && this.props.webPartDisplayMode === DisplayMode.Edit) {
return (
<div className={styles.notification}>
<div className={styles.notificationIcon}>
<i className={css("ms-Icon ms-Icon--ErrorBadge", styles.notificationIcon)} aria-hidden="true"></i>
<span className={styles.notificationHeader}>
Edit Mode
</span>
</div>
</div>);
}
if (!this._isConfigurationValid && this.props.webPartDisplayMode === DisplayMode.Read) {
return (
<div>
<div className={styles.notification}>
<div className={styles.notificationIcon}>
<i className={css("ms-Icon ms-Icon--ErrorBadge", styles.notificationIcon)} aria-hidden="true"></i>
<span className={styles.notificationHeader}>
Preview Mode
</span>
</div>
</div>
</div>);
}
if (this._isConfigurationValid) {
if (this.state.isLoading) {
if (SpinnerSize && SpinnerSize.large) {
return (<div className={styles.loadingWrapper}>
<Spinner size={SpinnerSize.large} label='Loading documents...' />
</div>);
}
}
else {
if (this.state.isErrorOccured) {
return (<div>
<div className={styles.notification}>
<div className={styles.notificationIcon}>
<i className={css("ms-Icon ms-Icon--ErrorBadge", styles.notificationIcon)} aria-hidden="true"></i>
<span className={styles.notificationHeader}>
Something went wrong...
</span>
</div>
<div className={styles.notificationDescription}>
<span>
{this.state.errorMessage}
</span>
</div>
</div>
</div>);
}
return (
<div>
<div className={styles.topBar}>
<span className={styles.topBarText}>{this.props.title}</span>
<div className={styles.topBarFilters} >
{this.state.showResetFilters && (
<span className={styles.resetFilter}>
<i className="ms-Icon ms-Icon--ClearFilter"
aria-hidden="false"
role="button"
onClick={this._onResetFiltersClicked} >
</i>
</span>
)}
</div>
</div>
<div>
<MarqueeSelection selection={this._selection}>
<DetailsList
className={styles.docsWrapper}
columns={this.state.columns}
items={this.state.displayedDocuments}
onItemInvoked={(item) => { this._openDocument(item, this); }}
selectionPreservedOnEmptyClick={true}
onRenderItemColumn={this._renderItemColumn}
checkboxVisibility={CheckboxVisibility.hidden} />
{contextualMenuProps && (
<ContextualMenu { ...contextualMenuProps } />
)}
</MarqueeSelection>
</div>
</div>
);
}
}
}
public componentDidMount() {
debugger;
if (this._isConfigurationValid) {
if (this.props.useSearchData) {
this.props.dataProvider.readDocumentsFromSearch().then(
(documents: IDocument[]) => {
debugger;
this.setState({
allDocuments: documents,
displayedDocuments: documents,
isLoading: false,
columns: this.state.columns
});
},
(data: any) => {
this.setState({
allDocuments: [],
displayedDocuments: [],
isLoading: false,
isErrorOccured: true,
errorMessage: data
});
}).catch((ex) => {
this.setState({
allDocuments: [],
displayedDocuments: [],
isLoading: false,
isErrorOccured: true,
errorMessage: ex.errorMessage
});
});
}
else {
this.props.dataProvider.readDocumentsFromLibrary().then(
//resolve
(documents: IDocument[]) => {
debugger;
this.setState({
allDocuments: documents,
displayedDocuments: documents,
isLoading: false,
columns: this.state.columns
});
},
//reject
(data: any) => {
this.setState({
allDocuments: [],
displayedDocuments: [],
isLoading: false,
isErrorOccured: true,
errorMessage: data
});
}
).catch((ex) => {
debugger;
this.setState({
allDocuments: [],
displayedDocuments: [],
isLoading: false,
isErrorOccured: true,
errorMessage: ex.errorMessage
});
});
}
}
}
/**
* Specify the columns and their properties
*/
private _setupColumns(): IColumn[] {
const columnsSingleClient: IColumn[] =
[{
key: 'FileIcon',
name: '',
fieldName: 'FileIcon',
minWidth: 20,
maxWidth: 20,
isResizable: true,
data: String
},
{
key: 'Name',
name: 'Name',
fieldName: 'Name',
minWidth: 100,
isResizable: true,
isSorted: false,
isSortedDescending: false,
columnActionsMode: ColumnActionsMode.hasDropdown,
onColumnClick: this._onColumnClick,
onColumnContextMenu: this._onColumnContextMenu,
data: String
},
{
key: 'ContentType',
name: 'Content Type',
fieldName: 'ContentType',
minWidth: 80,
isResizable: true,
isSorted: false,
isSortedDescending: false,
columnActionsMode: ColumnActionsMode.hasDropdown,
onColumnClick: this._onColumnClick,
onColumnContextMenu: this._onColumnContextMenu,
data: String
},
{
key: 'Id',
name: 'ID',
fieldName: 'Id',
minWidth: 60,
isResizable: true,
isSorted: false,
isSortedDescending: false,
columnActionsMode: ColumnActionsMode.hasDropdown,
onColumnClick: this._onColumnClick,
onColumnContextMenu: this._onColumnContextMenu,
data: Number
},
{
key: 'VersionString',
name: 'Version',
fieldName: 'VersionString',
minWidth: 60,
isResizable: true,
isSorted: false,
isSortedDescending: false,
data: String
},
{
key: 'Modified',
name: 'Modified',
fieldName: 'Modified',
minWidth: 80,
isResizable: true,
isSorted: false,
isSortedDescending: false,
columnActionsMode: ColumnActionsMode.hasDropdown,
onColumnClick: this._onColumnClick,
onColumnContextMenu: this._onColumnContextMenu,
data: Date
},
{
key: 'ModifiedBy',
name: 'Modified By',
fieldName: 'ModifiedBy',
minWidth: 80,
isResizable: true,
isSorted: false,
isSortedDescending: false,
columnActionsMode: ColumnActionsMode.hasDropdown,
onColumnClick: this._onColumnClick,
onColumnContextMenu: this._onColumnContextMenu,
data: String
},
];
return columnsSingleClient;
}
private _onResetFiltersClicked() {
let columns = this.state.columns;
//reset the columns
_.map(columns, (c: IColumn) => {
c.isSorted = false;
c.isSortedDescending = false;
c.isFiltered = false;
});
//update the state, this will force the control to refresh
this.setState({
displayedDocuments: this.state.allDocuments,
showResetFilters: false,
columns: columns
});
}
private _renderItemColumn(item, index, column) {
//here we can add column specific logic
// - image control for the FileIcon column
// - render link for the Name column
let fieldContent = item[column.fieldName];
switch (column.key) {
case 'FileIcon':
return <Image src={fieldContent} width={16} height={16} imageFit={ImageFit.center} />;
case 'Name':
return <Link data-selection-invoke={true} >{item[column.key]}</Link>;
default:
return <span>{fieldContent}</span>;
}
}
private _openDocument(docItem: IDocument, thisContext: any): void {
debugger;
let utility = new Utils();
utility.OpenDocument(docItem, thisContext, true);
}
@autobind
private _onSortColumn(column: IColumn, isSortedDescending: boolean) {
column = _.find(this.state.columns, c => c.fieldName === column.fieldName);
column.isSortedDescending = isSortedDescending;
column.isSorted = true;
//reset the other columns
let modifeidColumns: IColumn[] = this.state.columns;
_.map(modifeidColumns, (c: IColumn) => {
if (c.fieldName != column.fieldName) {
c.isSorted = false;
c.isSortedDescending = false;
}
});
let modifiedDocs = this.state.displayedDocuments;
modifiedDocs = _.orderBy(
modifiedDocs,
[(document) => {
console.log(document[column.fieldName]);
console.log(typeof (document[column.fieldName]));
if (column.data == Number) {
if (document[column.fieldName]) {
return parseInt(document[column.fieldName]);
}
return 0;
}
if (column.data == Date) {
if (document[column.fieldName]) {
return new Date(document[column.fieldName]);
}
return new Date(0);
}
return document[column.fieldName];
}],
[column.isSortedDescending ? "desc" : "asc"]);
this.setState({
displayedDocuments: modifiedDocs,
showResetFilters: true,
columns: modifeidColumns
});
}
@autobind
public ClickFilter(ev?: React.MouseEvent<HTMLElement>, item?: IContextualMenuItem): void {
debugger;
if (item) {
let columns = this.state.columns;
columns.filter(matchColumn => matchColumn.key === item.data)
.forEach((filteredColumn: IColumn) => {
filteredColumn.isFiltered = true;
});
let documents = this.state.displayedDocuments;
let newDocs = [];
if (item.data != "Tags") {
newDocs = documents.filter(matchDoc => matchDoc[item.data] === item.key);
}
else {
for (let i = 0; i < documents.length; i++) {
let itemValue: string = documents[i][item.data];
if (itemValue.indexOf(item.key) > -1) {
newDocs.push(documents[i]);
}
}
}
this.setState({ displayedDocuments: newDocs, showResetFilters: true });
}
}
@autobind
private _onColumnClick(ev: React.MouseEvent<HTMLElement>, column: IColumn) {
debugger;
if (column.columnActionsMode !== ColumnActionsMode.disabled) {
this.setState({
contextualMenuProps: this._getContextualMenuProps(ev, column)
});
}
}
@autobind
private _onColumnContextMenu(column: IColumn, ev: React.MouseEvent<HTMLElement>) {
debugger;
if (column.columnActionsMode !== ColumnActionsMode.disabled) {
this.setState({
contextualMenuProps: this._getContextualMenuProps(ev, column)
});
}
}
private _getContextualMenuProps(ev: React.MouseEvent<HTMLElement>, column: IColumn): IContextualMenuProps {
debugger;
let utility = new Utils();
let items: IContextualMenuItem[] = utility.GetSortingMenuItems(column, this._onSortColumn);
if (this.isFilterable(column.key)) {
items.push({
key: 'filterBy',
name: 'Filter by ',// + column.name,
canCheck: true,
checked: column.isFiltered,
subMenuProps: {
items: this._GetFilterValues(column)
}
});
}
return {
items: items,
targetElement: ev.currentTarget as HTMLElement,
directionalHint: DirectionalHint.bottomLeftEdge,
gapSpace: 10,
isBeakVisible: true,
onDismiss: this._onContextualMenuDismissed
};
}
private _onContextualMenuDismissed() {
this.setState({
contextualMenuProps: null
});
}
private _GetFilterValues(column: IColumn): IContextualMenuItem[] {
debugger;
let utility = new Utils();
let filters = utility.GetFilterValues(column, this.state.displayedDocuments, this.ClickFilter);
return filters;
}
// one approach of how to control where to have filter menu
private isFilterable(columnKey: string): boolean {
return columnKey != "Name";
}
}

View File

@ -0,0 +1,12 @@
import IDataProvider from '../../../dataproviders/IDataProvider';
import { DisplayMode } from '@microsoft/sp-core-library';
export interface IDocumentsProps {
title: string;
webPartDisplayMode: DisplayMode;
dataProvider: IDataProvider;
useSearchData: boolean;
}

View File

@ -0,0 +1,20 @@
import { IDocument } from '../../../common/IObjects';
import { IColumn, IContextualMenuProps } from 'office-ui-fabric-react';
interface IDocumentsState {
allDocuments?: IDocument[];
displayedDocuments?: IDocument[];
showResetFilters?: boolean;
isLoading?: boolean;
columns?: IColumn[];
showPanel?: boolean;
panelDocUrl?: string;
panelTitle?: string;
contextualMenuProps?: IContextualMenuProps;
//???
isErrorOccured?: boolean;
errorMessage?: string;
}
export default IDocumentsState;

View File

@ -0,0 +1,11 @@
import { IDocument } from "../common/IObjects";
export default interface IDataProvider {
validateSettings(): boolean;
readDocumentsFromSearch(): Promise<IDocument[]>;
readDocumentsFromLibrary(): Promise<IDocument[]>;
}

View File

@ -0,0 +1,161 @@
import { IDocument } from "../common/IObjects";
import IDataProvider from './IDataProvider';
import { Utils } from '../common/Utils';
import { IWebPartContext } from '@microsoft/sp-webpart-base';
export default class MockupDataProvider implements IDataProvider {
private _libraryAbsoluteUrl: string;
constructor(libraryUrl: string) {
if (libraryUrl) {
this._libraryAbsoluteUrl = libraryUrl;
}
}
public validateSettings(): boolean {
if (!this._libraryAbsoluteUrl) {
return false;
}
return true;
}
/**
* Returns sample data
*/
public readDocumentsFromSearch(): Promise<IDocument[]> {
let utility = new Utils();
let today: Date = new Date();
let date2: Date = new Date();
date2.setDate(today.getDate() - 5);
let _items: IDocument[] = [
{
Id: 1,
Name: "Test File.pdf",
FileRef: "/somesite/library/Test File.pdf",
Modified: utility.GetFormatedDate(today),
ModifiedBy: "Dimcho Tsanov",
ContentType: "Document",
FileIcon: utility.GetImgUrl("Test File.pdf"),
VersionString: "1.2",
},
{
Id: 2,
Name: "Report 2017-09.xlsx",
FileRef: "/somesite/library/Report 2017-09.xlsx",
Modified: utility.GetFormatedDate(today),
ModifiedBy: "Dimcho Tsanov",
ContentType: "Document",
FileIcon: utility.GetImgUrl("Report 2017-09.xlsx"),
VersionString: "1.0",
},
{
Id: 3,
Name: "Report 2017-10.xlsx",
FileRef: "/somesite/library/Report 2017-10.xlsx",
Modified: utility.GetFormatedDate(date2),
ModifiedBy: "Velin Georgiev",
ContentType: "Report",
FileIcon: utility.GetImgUrl("Report 2017-10.xlsx"),
VersionString: "11.0",
},
{
Id: 4,
Name: "Report 2016-11.xlsx",
FileRef: "/somesite/library/Report 2016-11.xlsx",
Modified: utility.GetFormatedDate(date2),
ModifiedBy: "Velin Georgiev",
ContentType: "Meeting Minutes",
FileIcon: utility.GetImgUrl("Report 2016-11.xlsx"),
VersionString: "1.2",
},
{
Id: 5,
Name: "Test File_final.pdf",
FileRef: "/somesite/library/Test File_final.pdf",
Modified: utility.GetFormatedDate(date2),
ModifiedBy: "Yana Tsanova",
ContentType: "Meeting Minutes",
FileIcon: utility.GetImgUrl("Test File_final.pdf"),
VersionString: "1.2",
},
];
return new Promise<IDocument[]>((resolve) => {
setTimeout(() => {
resolve(_items);
}, 2000);
});
}
/**
* Returns sample data
*/
public readDocumentsFromLibrary(): Promise<IDocument[]> {
let utility = new Utils();
let today: Date = new Date();
let date2: Date = new Date();
date2.setDate(today.getDate() - 5);
let _items: IDocument[] = [
{
Id: 1,
Name: "Test File.pdf",
FileRef: "/somesite/library/Test File.pdf",
Modified: utility.GetFormatedDate(today),
ModifiedBy: "Dimcho Tsanov",
ContentType: "Document",
FileIcon: utility.GetImgUrl("Test File.pdf"),
VersionString: "1.2",
},
{
Id: 2,
Name: "Report 2017-09.xlsx",
FileRef: "/somesite/library/Report 2017-09.xlsx",
Modified: utility.GetFormatedDate(today),
ModifiedBy: "Dimcho Tsanov",
ContentType: "Document",
FileIcon: utility.GetImgUrl("Report 2017-09.xlsx"),
VersionString: "1.0",
},
{
Id: 3,
Name: "Report 2017-10.xlsx",
FileRef: "/somesite/library/Report 2017-10.xlsx",
Modified: utility.GetFormatedDate(date2),
ModifiedBy: "Velin Georgiev",
ContentType: "Report",
FileIcon: utility.GetImgUrl("Report 2017-10.xlsx"),
VersionString: "11.0",
},
{
Id: 4,
Name: "Report 2016-11.xlsx",
FileRef: "/somesite/library/Report 2016-11.xlsx",
Modified: utility.GetFormatedDate(date2),
ModifiedBy: "Velin Georgiev",
ContentType: "Meeting Minutes",
FileIcon: utility.GetImgUrl("Report 2016-11.xlsx"),
VersionString: "1.2",
},
{
Id: 5,
Name: "Test File_final.pdf",
FileRef: "/somesite/library/Test File_final.pdf",
Modified: utility.GetFormatedDate(date2),
ModifiedBy: "Yana Tsanova",
ContentType: "Meeting Minutes",
FileIcon: utility.GetImgUrl("Test File_final.pdf"),
VersionString: "1.2",
},
];
return new Promise<IDocument[]>((resolve) => {
setTimeout(() => {
resolve(_items);
}, 2000);
});
}
}

View File

@ -0,0 +1,238 @@
import { IWebPartContext } from '@microsoft/sp-webpart-base';
import { SPHttpClient } from '@microsoft/sp-http';
import { IDocument } from "../common/IObjects";
import IDataProvider from "./IDataProvider";
import { Utils } from '../common/Utils';
export default class SharePointDataProvider implements IDataProvider {
private _webPartContext: IWebPartContext;
private _libraryAbsoluteUrl: string;
private _webAbsoluteUrl: string;
constructor(value: IWebPartContext, libraryUrl: string) {
this._webPartContext = value;
this._libraryAbsoluteUrl =
libraryUrl.lastIndexOf("/") == libraryUrl.length - 1 ?
libraryUrl.substr(0, libraryUrl.length - 1) :
libraryUrl;
this._webAbsoluteUrl = value.pageContext.web.absoluteUrl;
}
/**
* Check is all settings passed in the constructor are correctly initialized
*/
public validateSettings(): boolean {
if (!this._libraryAbsoluteUrl) {
return false;
}
return true;
}
/**
* Returns all documents from the Search index where the Path contains the library url
* Note: Library url is passed as parameter in the constructor
*/
public readDocumentsFromSearch(): Promise<IDocument[]> {
let utility = new Utils();
let searchQuery = '(path:"' + encodeURIComponent(this._libraryAbsoluteUrl) + '*")AND(IsDocument:1)';
let webAbsoluteUrl = this._webPartContext.pageContext.web.absoluteUrl;
const searchRequestUrl1: string = `${webAbsoluteUrl}/_api/search/query?querytext='${searchQuery}'` +
"&selectproperties='DocId,ContentType,ModifiedBy,LastModifiedTime,FileExtension,Path,SPWebURL,UIVersionStringOWSTEXT,UniqueId'";
// log in the console for debugging purpose
console.log(searchQuery);
return this._webPartContext.spHttpClient.get(
searchRequestUrl1,
SPHttpClient.configurations.v1,
{
headers: {
"odata-version": "3.0",
"accept": "application/json;odata=verbose"
},
method: "GET"
})
.then((response: any) => {
debugger;
if (response.status >= 200 && response.status < 300) {
return response.json();
} else {
return Promise.reject(new Error(JSON.stringify(response)));
}
}).then((response: any) => {
debugger;
//convert the reuselts in object with properties
let results: any[] = response.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;
var obj = [];
for (let l = 0; l < results.length; l++) {
var cells = results[l].Cells.results;
var cell = {};
for (let m = 0; m < cells.length; m++) {
cell[cells[m].Key] = cells[m].Value;
}
obj.push(cell);
}
// use the search results as objects
let docs: IDocument[] = [];
for (let i = 0; i < obj.length; i++) {
docs.push({
Id: parseInt(obj[i].DocId),
FileRef: utility.GetFileRef(obj[i].OriginalPath),
Modified: utility.GetFormatedDateString(obj[i].LastModifiedTime),
ModifiedBy: obj[i].ModifiedBy,
FileIcon: utility.GetImgUrlByFileExtension(obj[i].FileExtension),
Name: utility.GetFileName(obj[i].Path),
VersionString: obj[i].UIVersionStringOWSTEXT,
ContentType: utility.GetContentType(obj[i].ContentType),
ParentWebUrl: obj[i].SPWebURL,
UniqueId: obj[i].UniqueId.replace("{", "").replace("}", "")
});
}
return docs;
});
/*
NOTE:
the above code use get request for retrieving the search results; alternatively, you can use POST request
Sample code:
var body = {
'request': {
'__metadata': { 'type': 'Microsoft.Office.Server.Search.REST.SearchRequest' },
'Querytext': searchQuery,
'RowLimit': '100',
'TrimDuplicates': 'False',
'SelectProperties': {
'results':
['DocId', 'ModifiedBy', 'OriginalPath', 'LastModifiedTime', 'FileExtension', 'Path', 'SPWebURL']
}
}
};
const searchRequestUrl: string = `${webAbsoluteUrl}/_api/search/postquery`;
return this._webPartContext.spHttpClient.post(
searchRequestUrl,
SPHttpClient.configurations.v1,
{
headers: {
"odata-version": "3.0",
"accept": "application/json;odata=verbose"
},
body: JSON.stringify(body),
method: "POST"
})
*/
}
/**
* Returns all documents from the library
* Note: Library url is passed as parameter in the constructor
*/
public readDocumentsFromLibrary(): Promise<IDocument[]> {
debugger;
let utility = new Utils();
let libraryRelativeUrl = utility.GetRelativePathFromAbsolute(this._libraryAbsoluteUrl);
return this._readListId(libraryRelativeUrl).then((listId: string): Promise<IDocument[]> => {
const queryUrlGetAllItems: string = this._webAbsoluteUrl + `/_api/web/lists(guid'${listId}')/Items` +
"?$select=ID,DocIcon,FileLeafRef,FileRef,Modified,UniqueId,OData__UIVersionString,ContentTypeId,ContentType/Name,Editor/Title&$expand=Editor,ContentType";
/*
The above query will get all items, including folders and items in the folders.
After that we remove those items, that are not based on the Document Content Type.
Depending on your logic, you can use different endpoints, like:
/_api/web/lists(guid'${listId}')/GetItems(query=@v1)?@v1={"FolderServerRelativeUrl" : "${libraryRelativeUrl}", "ViewXml":"<View Scope='RecursiveAll'></View>"}
/_api/web/GetFolderByServerRelativePath(decodedurl='${libraryRelativeUrl}')?$select=ID,FileLeafRef,FileRef,ModifiedBy&$expand=Files,ModifiedBy
/_api/web/GetFolderByServerRelativeUrl('${libraryRelativeUrl}')/Files?$expand=ListItemAllFields
*/
return this._webPartContext.spHttpClient.get(
queryUrlGetAllItems,
SPHttpClient.configurations.v1)
.then(
(response: any) => {
if (response.status >= 200 && response.status < 300) {
return response.json();
} else {
return Promise.reject(new Error(JSON.stringify(response)));
}
})
.then((data: any) => {
debugger;
let documents: IDocument[] = [];
if (data) {
for (let i = 0; i < data.value.length; i++) {
let item = data.value[i];
//check the content type; Include only documents in the response
if (item.ContentTypeId.indexOf("0x0101") == 0) {
var doc: IDocument = {
Id: item.Id,
FileRef: item.FileRef,
Name: item.FileLeafRef,
VersionString: item.OData__UIVersionString,
ContentType: item.ContentType.Name,
ModifiedBy: item.Editor.Title,
Modified: utility.GetFormatedDateString(item.Modified),
UniqueId: item.UniqueId,
ParentWebUrl: this._webAbsoluteUrl,// this will work in case the library is in the same web as the web part!
//icon for the Folder content type is a different
FileIcon: item.ContentType.Name != "Folder" ? utility.GetImgUrlByFileExtension(item.DocIcon) : utility.GetImgUrlByFileExtension("folder")
};
documents.push(doc);
}
}
}
return documents;
}).catch((ex) => {
console.log("readDocumentsFromLibrary > spHttpClient.get()...catch:", ex);
throw ex;
});
});
}
// Helper Methods
/**
* Returns the list's ID based on its site relative url
* listRelativeUrl format: '/sites/mysite/shared documents'
* returned value: Guid if succeeded, otherwise - empty string
*/
private _readListId(listRelativeUrl: string): Promise<string> {
let queryUrlGetList = this._webAbsoluteUrl + "/_api/web/GetFolderByServerRelativePath(decodedurl='" + decodeURIComponent(listRelativeUrl) + "')/Properties";
return this._webPartContext.spHttpClient.get(
queryUrlGetList,
SPHttpClient.configurations.v1)
.then(
(response: any) => {
if (response.status >= 200 && response.status < 300) {
return response.json();
} else {
return Promise.reject(new Error(JSON.stringify(response)));
}
})
.then((data: any) => {
debugger;
if (data) {
let listIdValue: string = data.vti_x005f_listname; // string format '{00000000-0000-0000-0000-000000000000}'
let listId = listIdValue.replace("{", "").replace("}", "");
return listId;
}
else {
console.log("no list info");
}
return "";
}).catch((ex) => {
console.log("_readListId > spHttpClient.get()...catch:", ex);
throw ex;
});
}
}

View File

@ -0,0 +1,31 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "8635c52e-cf0b-4c6d-a5ba-66f1d8d92005",
"alias": "LibraryDocumentsWebPart",
"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", // Other
"group": {
"default": "SPFx Samples"
},
"title": {
"default": "Library Documents"
},
"description": {
"default": "A sample webpart that displays library documents in Office UI Fabric DetailsList."
},
"officeFabricIconFontName": "Page",
"properties": {
"libraryUrl": ""
}
}
]
}

View File

@ -0,0 +1,83 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, Environment, EnvironmentType } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import Documents from '../../components/documentsList/component/Documents';
import { IDocumentsProps } from '../../components/documentsList/component/IDocumentsProps';
import IDataProvider from '../../dataproviders/IDataProvider';
import SharePointDataProvider from '../../dataproviders/SharePointDataProvider';
import MockupDataProvider from '../../dataproviders/MockupDataProvider';
import * as strings from 'LibraryDocumentsWebPartStrings';
export interface ILibraryDocumentsWebPartProps {
libraryUrl: string;
}
export default class LibraryDocumentsWebPart extends BaseClientSideWebPart<ILibraryDocumentsWebPartProps> {
private _dataProvider: IDataProvider;
protected onInit(): Promise<void> {
debugger;
if (DEBUG && Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockupDataProvider(this.properties.libraryUrl);
} else {
if (this.properties.libraryUrl) {
this._dataProvider = new SharePointDataProvider(this.context, this.properties.libraryUrl);
}
else {
//the WebPart property is not filled
//do nothing, the Documents component will display notification message
}
}
return super.onInit();
}
public render(): void {
const element: React.ReactElement<IDocumentsProps> = React.createElement(
Documents,
{
title: "Library Documents",
useSearchData: false,
webPartDisplayMode: this.displayMode,
dataProvider: this._dataProvider
}
);
ReactDom.render(element, 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('libraryUrl', {
label: strings.LibraryUrlFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"LibraryUrlFieldLabel": "Add a library absolute url:"
}
});

View File

@ -0,0 +1,10 @@
declare interface ILibraryDocumentsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
LibraryUrlFieldLabel: string;
}
declare module 'LibraryDocumentsWebPartStrings' {
const strings: ILibraryDocumentsWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('LibraryDocumentsWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,31 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "e7054e6c-a83c-4522-9cb1-b311381142f4",
"alias": "SearchDocumentsWebPart",
"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", // Other
"group": {
"default": "SPFx Samples"
},
"title": {
"default": "Search Documents"
},
"description": {
"default": "A sample webpart that displays search documents in Office UI Fabric DetailsList."
},
"officeFabricIconFontName": "Page",
"properties": {
"libraryUrl": ""
}
}
]
}

View File

@ -0,0 +1,82 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, Environment, EnvironmentType } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import Documents from '../../components/documentsList/component/Documents';
import { IDocumentsProps } from '../../components/documentsList/component/IDocumentsProps';
import IDataProvider from '../../dataproviders/IDataProvider';
import SharePointDataProvider from '../../dataproviders/SharePointDataProvider';
import MockupDataProvider from '../../dataproviders/MockupDataProvider';
import * as strings from 'SearchDocumentsWebPartStrings';
export interface ISearchDocumentsWebPartProps {
libraryUrl: string;
}
export default class SearchDocumentsWebPart extends BaseClientSideWebPart<ISearchDocumentsWebPartProps> {
private _dataProvider: IDataProvider;
protected onInit(): Promise<void> {
debugger;
if (DEBUG && Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockupDataProvider(this.properties.libraryUrl);
} else {
if (this.properties.libraryUrl) {
this._dataProvider = new SharePointDataProvider(this.context, this.properties.libraryUrl);
}
else {
//the WebPart property is not filled
//do nothing, the Documents component will display notification message
}
}
return super.onInit();
}
public render(): void {
const element: React.ReactElement<IDocumentsProps> = React.createElement(
Documents,
{
title: "Search Documents",
useSearchData: true,
webPartDisplayMode: this.displayMode,
dataProvider: this._dataProvider
}
);
ReactDom.render(element, 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('libraryUrl', {
label: strings.LibraryUrlFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"LibraryUrlFieldLabel": "Add library absolute url:"
}
});

View File

@ -0,0 +1,10 @@
declare interface ISearchDocumentsWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
LibraryUrlFieldLabel: string;
}
declare module 'SearchDocumentsWebPartStrings' {
const strings: ISearchDocumentsWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('SearchDocumentsWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />