Merge pull request #1798 from chandaniprajapati/react-datatable-fluentui
react-datatable: fluent ui design and added some advanced features
This commit is contained in:
commit
6da1e3d6a7
|
@ -42,6 +42,9 @@ Version|Date|Comments
|
|||
1.0|February 19, 2021|Initial release
|
||||
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.
|
||||
|
||||
|
||||
## 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.**
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -25,9 +25,8 @@
|
|||
"@pnp/spfx-controls-react": "^2.4.0",
|
||||
"@pnp/spfx-property-controls": "^2.3.0",
|
||||
"export-to-csv": "^0.2.1",
|
||||
"jspdf": "^2.3.0",
|
||||
"jspdf-autotable": "^3.5.13",
|
||||
"office-ui-fabric-react": "6.214.0",
|
||||
"pdfmake": "^0.1.70",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.exportToCsv {
|
||||
.btnCSV {
|
||||
background-color: "[theme:themePrimary, default:#0078d7]";
|
||||
border-color: "[theme: themePrimary, default: #0078d7]";
|
||||
color: "[theme:white, default:white]"
|
||||
}
|
||||
}
|
|
@ -1,20 +1,23 @@
|
|||
import * as React from 'react';
|
||||
import * as strings from 'ReactDatatableWebPartStrings';
|
||||
import { ExportToCsv } from 'export-to-csv';
|
||||
import { Button } from '@material-ui/core';
|
||||
import GetAppSharpIcon from '@material-ui/icons/GetAppSharp';
|
||||
import { IIconProps, PrimaryButton } from 'office-ui-fabric-react';
|
||||
import styles from './ExportListItemsToCSV.module.scss';
|
||||
interface IExportToCSV {
|
||||
columnHeader: Array<string>;
|
||||
listItems: any[];
|
||||
listName: string;
|
||||
description: string;
|
||||
dataSource: ()=> any[];
|
||||
}
|
||||
|
||||
export function ExportListItemsToCSV(props: IExportToCSV) {
|
||||
|
||||
let { columnHeader, listItems, listName, description } = props;
|
||||
const downloadIcon: IIconProps = { iconName: 'Download' };
|
||||
|
||||
let { columnHeader, listName, dataSource } = props;
|
||||
|
||||
function generateCSV() {
|
||||
|
||||
let colHeader = columnHeader;
|
||||
const options = {
|
||||
filename: listName,
|
||||
|
@ -30,15 +33,15 @@ export function ExportListItemsToCSV(props: IExportToCSV) {
|
|||
headers: colHeader
|
||||
};
|
||||
const csvExporter = new ExportToCsv(options);
|
||||
csvExporter.generateCsv(listItems);
|
||||
csvExporter.generateCsv(dataSource());
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="contained"
|
||||
<PrimaryButton
|
||||
text={strings.DownloadAsCSVLabel}
|
||||
iconProps={downloadIcon}
|
||||
onClick={() => generateCSV()}
|
||||
startIcon={<GetAppSharpIcon />}>
|
||||
{strings.DownloadAsCSVLabel}
|
||||
</Button>
|
||||
className={styles.btnCSV}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
export function csvCellFormatter(value: any, type: string) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
switch (type) {
|
||||
case 'SP.FieldUrl':
|
||||
value = value.props.children;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.exportToPdf {
|
||||
.btnPDF {
|
||||
background-color: "[theme:themePrimary, default:#0078d7]";
|
||||
border-color: "[theme: themePrimary, default: #0078d7]";
|
||||
color: "[theme:white, default:white]"
|
||||
}
|
||||
}
|
|
@ -1,29 +1,70 @@
|
|||
import * as React from 'react';
|
||||
import * as strings from 'ReactDatatableWebPartStrings';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { Button } from '@material-ui/core';
|
||||
import GetAppSharpIcon from '@material-ui/icons/GetAppSharp';
|
||||
import pdfMake from 'pdfmake/build/pdfmake';
|
||||
import pdfFonts from 'pdfmake/build/vfs_fonts';
|
||||
pdfMake.vfs = pdfFonts.pdfMake.vfs;
|
||||
import { IIconProps, PrimaryButton } from 'office-ui-fabric-react';
|
||||
import styles from './ExportListItemsToPDF.module.scss';
|
||||
import { isNullOrUndefined } from '../../utilities/utilities';
|
||||
import { IPropertyPaneDropdownOption } from '@microsoft/sp-property-pane';
|
||||
|
||||
interface IExportToPDF {
|
||||
htmlElementForPDF: string;
|
||||
listName: string;
|
||||
title: string;
|
||||
columns: any[];
|
||||
oddRowColor?: string;
|
||||
evenRowColor?: string;
|
||||
dataSource: ()=> any[];
|
||||
}
|
||||
|
||||
export function ExportListItemsToPDF(props: IExportToPDF) {
|
||||
|
||||
let { htmlElementForPDF, listName } = props;
|
||||
const downloadIcon: IIconProps = { iconName: 'Download' };
|
||||
|
||||
let { title, listName, columns, oddRowColor, evenRowColor, dataSource } = props;
|
||||
|
||||
function genearatePDF() {
|
||||
const doc = new jsPDF();
|
||||
autoTable(doc, { html: htmlElementForPDF, theme: 'grid' });
|
||||
doc.save(`${listName}.pdf`);
|
||||
}
|
||||
|
||||
let dataTableRows = dataSource().map(lItem => columns.reduce((arr, c) => [...arr, isNullOrUndefined(lItem[c]) ? '' : lItem[c]], []));
|
||||
|
||||
let data = {
|
||||
content: [
|
||||
{
|
||||
text: title,
|
||||
fontSize: 16,
|
||||
alignment: 'center',
|
||||
margin: [0, 0, 0, 15]
|
||||
},
|
||||
{
|
||||
style: 'tableExample',
|
||||
table: {
|
||||
widths: new Array(columns.length).fill("auto"),
|
||||
headerRows: 1,
|
||||
body: [
|
||||
columns.map(c=> ({text: c, bold: true})),
|
||||
...dataTableRows
|
||||
]
|
||||
},
|
||||
layout: {
|
||||
fillColor: function (rowIndex: number) {
|
||||
if (oddRowColor && evenRowColor)
|
||||
return (rowIndex % 2 === 0) ? evenRowColor : oddRowColor;
|
||||
else
|
||||
return (rowIndex % 2 === 0) ? '#CCCCCC' : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
pdfMake.createPdf(data).download(`${listName}.pdf`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="contained"
|
||||
<PrimaryButton
|
||||
text={strings.DownloadAsPDFLabel}
|
||||
iconProps={downloadIcon}
|
||||
onClick={() => genearatePDF()}
|
||||
startIcon={<GetAppSharpIcon />}>
|
||||
{strings.DownloadAsPDFLabel}
|
||||
</Button>
|
||||
className={styles.btnPDF}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
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;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
.pagination{
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
float: right;
|
||||
|
||||
.rowsPerPage{
|
||||
float: left;
|
||||
}
|
||||
|
||||
.buttonStyle {
|
||||
min-width: auto;
|
||||
text-align: center;
|
||||
border: 2px solid white;
|
||||
&:not([class*="ms-Button--primary"]){
|
||||
background-color: #f0f0f0;
|
||||
&:hover{
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonStyle:disabled{
|
||||
color: lightgray;
|
||||
i{
|
||||
color: lightgray
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +1,177 @@
|
|||
import TablePagination from '@material-ui/core/TablePagination';
|
||||
import * as React from 'react';
|
||||
import * as React from "react";
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import styles from "./Pagination.module.scss";
|
||||
import { isEqual } from "lodash";
|
||||
import { Dropdown, DropdownMenuItemType, IDropdownOption, IDropdownProps } from 'office-ui-fabric-react/lib/Dropdown';
|
||||
|
||||
interface IPagination {
|
||||
colSpan: number;
|
||||
export interface IPaginationProps {
|
||||
/**
|
||||
* The page initial selected
|
||||
*/
|
||||
currentPage: number;
|
||||
/**
|
||||
* The total items for which you want to generate pagination
|
||||
*/
|
||||
totalItems: number;
|
||||
onPaginationUpdate: (pageNo: number, pageSize: number) => void;
|
||||
/**
|
||||
* When the page number change send the page number selected
|
||||
*/
|
||||
onChange: (pageNo: number, rowsPerPage: number) => void;
|
||||
/**
|
||||
* The number of pages showing before the icon
|
||||
*/
|
||||
limiter?: number;
|
||||
/**
|
||||
* Hide the quick jump to the first page
|
||||
*/
|
||||
hideFirstPageJump?: boolean;
|
||||
/**
|
||||
* Hide the quick jump to the last page
|
||||
*/
|
||||
hideLastPageJump?: boolean;
|
||||
/**
|
||||
* Limitir icon, by default is More icon
|
||||
*/
|
||||
limiterIcon?: string;
|
||||
|
||||
}
|
||||
|
||||
export function Pagination(props: IPagination) {
|
||||
export interface IPaginationState {
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
paginationElements: number[];
|
||||
limiter: number;
|
||||
rowsPerPage?: number;
|
||||
}
|
||||
export class Pagination extends React.Component<IPaginationProps, IPaginationState> {
|
||||
constructor(props: Readonly<IPaginationProps>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentPage: props.currentPage,
|
||||
paginationElements : [],
|
||||
limiter: props.limiter ? props.limiter : 3,
|
||||
totalPages: 0,
|
||||
rowsPerPage: 10
|
||||
};
|
||||
}
|
||||
|
||||
let { colSpan, totalItems, onPaginationUpdate } = props;
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(5);
|
||||
public componentDidMount(){
|
||||
let totalPages = this.getTotalPages(this.props.totalItems);
|
||||
const paginationElements = this.preparePaginationElements(totalPages);
|
||||
this.setState({totalPages, paginationElements});
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
onPaginationUpdate(page, rowsPerPage);
|
||||
}, [page, rowsPerPage]);
|
||||
private getTotalPages(totalItems: number) {
|
||||
return totalItems ? Math.ceil(totalItems / this.state.rowsPerPage) : 0;
|
||||
}
|
||||
|
||||
const handlePageChange = (event, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
public componentDidUpdate(prevProps: IPaginationProps) {
|
||||
let { currentPage, paginationElements, totalPages } = this.state;
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
if (prevProps.totalItems !== this.props.totalItems) {
|
||||
totalPages = this.getTotalPages(this.props.totalItems);
|
||||
paginationElements = this.preparePaginationElements(totalPages);
|
||||
currentPage = (currentPage > totalPages) ? totalPages : currentPage;
|
||||
}
|
||||
if (this.props.currentPage !== prevProps.currentPage) {
|
||||
currentPage = this.props.currentPage > totalPages ? totalPages : this.props.currentPage;
|
||||
}
|
||||
|
||||
return (
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25, { label: 'All', value: -1 }]}
|
||||
colSpan={colSpan}
|
||||
count={totalItems}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
SelectProps={{
|
||||
inputProps: { 'aria-label': 'rows per page' },
|
||||
native: true,
|
||||
}}
|
||||
onChangePage={handlePageChange}
|
||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
);
|
||||
if (!isEqual(this.state.currentPage, currentPage) || !isEqual(this.state.paginationElements, paginationElements) || !isEqual(this.state.totalPages, totalPages)) {
|
||||
this.setState({
|
||||
paginationElements,
|
||||
currentPage,
|
||||
totalPages
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IPaginationProps> {
|
||||
return (
|
||||
<div className={`${styles.pagination} pagination-container`}>
|
||||
{!this.props.hideFirstPageJump &&
|
||||
<DefaultButton
|
||||
disabled={this.props.currentPage === 1}
|
||||
className={`${styles.buttonStyle} pagination-button pagination-button_first`}
|
||||
onClick={() => this.onClick(1)}
|
||||
iconProps={{ iconName: "DoubleChevronLeft" }}> First
|
||||
</DefaultButton>
|
||||
}
|
||||
<DefaultButton
|
||||
disabled={this.props.currentPage === 1}
|
||||
className={`${styles.buttonStyle} pagination-button pagination-button_prev`}
|
||||
onClick={() => this.onClick(this.state.currentPage - 1)}
|
||||
iconProps={{ iconName: "ChevronLeft" }}> Prev
|
||||
</DefaultButton>
|
||||
{this.state.paginationElements.map((pageNumber) => this.renderPageNumber(pageNumber))}
|
||||
<DefaultButton
|
||||
disabled={this.state.totalPages === this.props.currentPage}
|
||||
className={`${styles.buttonStyle} pagination-button pagination-button_next`}
|
||||
onClick={() => this.onClick(this.state.currentPage + 1)}
|
||||
iconProps={{ iconName: "ChevronRight" }}> Next
|
||||
</DefaultButton>
|
||||
{!this.props.hideLastPageJump &&
|
||||
<DefaultButton
|
||||
disabled={this.state.totalPages === this.props.currentPage}
|
||||
className={`${styles.buttonStyle} pagination-button pagination-button_last`}
|
||||
onClick={() => this.onClick(this.state.totalPages)}
|
||||
iconProps={{ iconName: "DoubleChevronRight" }}> Last
|
||||
</DefaultButton>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private preparePaginationElements = (totalPages: number) => {
|
||||
let paginationElementsArray = [];
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
paginationElementsArray.push(i + 1);
|
||||
}
|
||||
return paginationElementsArray;
|
||||
}
|
||||
|
||||
private onClick = (page: number) => {
|
||||
this.setState({ currentPage: page });
|
||||
this.props.onChange(page, this.state.rowsPerPage);
|
||||
}
|
||||
|
||||
private renderPageNumber(pageNumber) {
|
||||
if (pageNumber === this.state.currentPage) {
|
||||
return (
|
||||
<PrimaryButton
|
||||
className={styles.buttonStyle}
|
||||
onClick={() => this.onClick(pageNumber)}
|
||||
text={pageNumber}>
|
||||
</PrimaryButton>
|
||||
);
|
||||
} else {
|
||||
if (!(pageNumber < this.state.currentPage - this.state.limiter || pageNumber > this.state.currentPage + this.state.limiter)) {
|
||||
return (
|
||||
<DefaultButton
|
||||
className={styles.buttonStyle}
|
||||
onClick={() => this.onClick(pageNumber)}
|
||||
text={pageNumber}>
|
||||
</DefaultButton>);
|
||||
}
|
||||
else if (!(pageNumber < this.state.currentPage - this.state.limiter - 1 || pageNumber > this.state.currentPage + this.state.limiter + 1)) {
|
||||
if (this.props.limiterIcon) {
|
||||
return (<DefaultButton
|
||||
className={styles.buttonStyle}
|
||||
onClick={() => this.onClick(pageNumber)}
|
||||
iconProps={{ iconName: this.props.limiterIcon ? this.props.limiterIcon : "More" }}>
|
||||
</DefaultButton>);
|
||||
}
|
||||
else {
|
||||
return (<DefaultButton
|
||||
className={styles.buttonStyle}
|
||||
onClick={() => this.onClick(pageNumber)}
|
||||
iconProps={{ iconName: this.props.limiterIcon ? this.props.limiterIcon : "More" }}>
|
||||
</DefaultButton>);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,9 @@ 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`);
|
||||
expandQuery.push(selectedFields[i].key);
|
||||
break;
|
||||
case 'SP.FieldLookup':
|
||||
selectQuery.push(`${selectedFields[i].key}/Title`);
|
||||
expandQuery.push(selectedFields[i].key);
|
||||
|
@ -59,4 +62,14 @@ export class SPService {
|
|||
Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserProfileUrl(loginName: string, propertyName: string) {
|
||||
try {
|
||||
const profileUrl = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName);
|
||||
return profileUrl;
|
||||
}
|
||||
catch (err) {
|
||||
Promise.reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
/** utility function to check null or undefined */
|
||||
export const isNullOrUndefined = (value: any) => value === null || value === undefined;
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
"default": "Other"
|
||||
},
|
||||
"title": {
|
||||
"default": "react-datatable"
|
||||
"default": "Datatable"
|
||||
},
|
||||
"description": {
|
||||
"default": "Shows the sharepoint list items in data table format with some advanced features."
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { ColDef } from "@material-ui/data-grid";
|
||||
import {
|
||||
IColumn,
|
||||
} from 'office-ui-fabric-react/lib/DetailsList';
|
||||
|
||||
|
||||
export interface IReactDatatableState {
|
||||
listItems: any[];
|
||||
columns: ColDef[];
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
searchText: string;
|
||||
contentType: string;
|
||||
sortingFields: string;
|
||||
sortDirection: 'asc'|'desc';
|
||||
columns: IColumn[];
|
||||
page: number;
|
||||
rowsPerPage?: number;
|
||||
searchText: string;
|
||||
contentType: string;
|
||||
sortingFields: string;
|
||||
pageOfItems: any[];
|
||||
sortDirection: 'asc' | 'desc';
|
||||
}
|
||||
|
|
|
@ -3,8 +3,16 @@
|
|||
.reactDatatable {
|
||||
.dataTableUtilities{
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
.downloadButtons > *{
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.txtSearchBox{
|
||||
min-width: 250px;
|
||||
}
|
||||
.colHeader{
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,34 +6,18 @@ import * as strings from 'ReactDatatableWebPartStrings';
|
|||
import { SPService } from '../../../shared/service/SPService';
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import { Alert, AlertTitle } from '@material-ui/lab';
|
||||
import TableContainer from '@material-ui/core/TableContainer';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TableFooter from '@material-ui/core/TableFooter';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import { Grid, InputAdornment, Link, TextField } from '@material-ui/core';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import { TextField } from 'office-ui-fabric-react/lib/TextField';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { Link, Text } from 'office-ui-fabric-react';
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import { withStyles, Theme, createStyles, makeStyles } from '@material-ui/core/styles';
|
||||
import TableSortLabel from '@material-ui/core/TableSortLabel';
|
||||
import { ExportListItemsToCSV } from '../../../shared/common/ExportListItemsToCSV/ExportListItemsToCSV';
|
||||
import { ExportListItemsToPDF } from '../../../shared/common/ExportListItemsToPDF/ExportListItemsToPDF';
|
||||
import { Pagination } from '../../../shared/common/Pagination/Pagination';
|
||||
import { DetailsList, DetailsListLayoutMode, DetailsRow, IDetailsRowStyles, IDetailsListProps, IColumn, MessageBar, SelectionMode } from 'office-ui-fabric-react';
|
||||
import { pdfCellFormatter } from '../../../shared/common/ExportListItemsToPDF/ExportListItemsToPDFFormatter';
|
||||
import { csvCellFormatter } from '../../../shared/common/ExportListItemsToCSV/ExportListItemsToCSVFormatter';
|
||||
import { IPropertyPaneDropdownOption } from '@microsoft/sp-property-pane';
|
||||
|
||||
const StyledTableCell = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
head: {
|
||||
backgroundColor: theme.palette.grey[200],
|
||||
},
|
||||
body: {
|
||||
fontSize: 14,
|
||||
},
|
||||
}),
|
||||
)(TableCell);
|
||||
|
||||
export default class ReactDatatable extends React.Component<IReactDatatableProps, IReactDatatableState> {
|
||||
|
||||
|
@ -44,12 +28,13 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
|
|||
this.state = {
|
||||
listItems: [],
|
||||
columns: [],
|
||||
page: 0,
|
||||
rowsPerPage: 5,
|
||||
page: 1,
|
||||
searchText: '',
|
||||
rowsPerPage: 10,
|
||||
sortingFields: '',
|
||||
sortDirection: 'asc',
|
||||
contentType: ''
|
||||
contentType: '',
|
||||
pageOfItems: []
|
||||
};
|
||||
this._services = new SPService(this.props.context);
|
||||
this._onConfigure = this._onConfigure.bind(this);
|
||||
|
@ -80,7 +65,16 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
|
|||
return ob;
|
||||
}, {})
|
||||
}));
|
||||
let dataGridColumns = [...fields].map(f => ({ field: f.key as string, headerName: f.text }));
|
||||
let dataGridColumns: IColumn[] = [...fields].map(f => ({
|
||||
key: f.key as string,
|
||||
name: f.text,
|
||||
fieldName: f.key as string,
|
||||
isResizable: true,
|
||||
onColumnClick: this.props.sortBy && this.props.sortBy.filter(field => field === f.key).length ? this.handleSorting(f.key as string) : undefined,
|
||||
minWidth: 70,
|
||||
maxWidth: 100,
|
||||
headerClassName: styles.colHeader
|
||||
}));
|
||||
this.setState({ listItems: listItems, columns: dataGridColumns });
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +82,7 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
|
|||
private _onConfigure() {
|
||||
this.props.context.propertyPane.open();
|
||||
}
|
||||
|
||||
|
||||
public formatColumnValue(value: any, type: string) {
|
||||
if (!value) {
|
||||
|
@ -129,6 +124,34 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
|
|||
}
|
||||
return value;
|
||||
}
|
||||
public formatValueForExportingData(value: any, type?: string) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
switch (type) {
|
||||
case 'SP.FieldUser':
|
||||
let userName = value['Title'];
|
||||
value = userName;
|
||||
break;
|
||||
case 'SP.FieldUrl':
|
||||
let url = value['Url'];
|
||||
let description = value['Description'];
|
||||
value = <a href={url}>{description}</a>;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
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;
|
||||
}, {})
|
||||
}));
|
||||
}
|
||||
|
||||
private handlePaginationChange(pageNo: number, pageSize: number) {
|
||||
this.setState({ page: pageNo, rowsPerPage: pageSize });
|
||||
|
@ -174,22 +197,40 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
|
|||
private paginateFn = (filterItem: any[]) => {
|
||||
let { rowsPerPage, page } = this.state;
|
||||
return (rowsPerPage > 0
|
||||
? filterItem.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
? filterItem.slice((page - 1) * rowsPerPage, (page - 1) * rowsPerPage + rowsPerPage)
|
||||
: filterItem
|
||||
);
|
||||
}
|
||||
|
||||
private handleSorting = (property: string) => (event: React.MouseEvent<unknown>) => {
|
||||
private handleSorting = (property: string) => (event: React.MouseEvent<unknown>, column: IColumn) => {
|
||||
property = column.key;
|
||||
let { sortingFields, sortDirection } = this.state;
|
||||
const isAsc = sortingFields === property && sortDirection === 'asc';
|
||||
this.setState({ sortDirection: (isAsc ? 'desc' : 'asc'), sortingFields: property });
|
||||
const isAsc = sortingFields && sortingFields === property && sortDirection === 'asc';
|
||||
let updateColumns = this.state.columns.map(c => {
|
||||
return c.key === property ? { ...c, isSorted: true, isSortedDescending: (isAsc ? false : true) } : { ...c, isSorted: false, isSortedDescending: true };
|
||||
});
|
||||
this.setState({ sortDirection: (isAsc ? 'desc' : 'asc'), sortingFields: property, columns: updateColumns });
|
||||
}
|
||||
|
||||
private _onRenderRow: IDetailsListProps['onRenderRow'] = props => {
|
||||
const customStyles: Partial<IDetailsRowStyles> = {};
|
||||
if (props) {
|
||||
if (props.itemIndex % 2 === 0) {
|
||||
customStyles.root = { backgroundColor: this.props.evenRowColor };
|
||||
}
|
||||
else {
|
||||
customStyles.root = { backgroundColor: this.props.oddRowColor };
|
||||
}
|
||||
return <DetailsRow {...props} styles={customStyles} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IReactDatatableProps> {
|
||||
let filteredItems = this.filterListItems();
|
||||
let { list, fields, enableDownloadAsCsv, enableDownloadAsPdf, enablePagination, displayMode, enableSearching, title, evenRowColor, oddRowColor, sortBy } = this.props;
|
||||
let { sortingFields, sortDirection, columns, listItems } = this.state;
|
||||
filteredItems = enablePagination ? this.paginateFn(filteredItems) : filteredItems;
|
||||
let { list, fields, enableDownloadAsCsv, enableDownloadAsPdf, enablePagination, displayMode, enableSearching, title, evenRowColor, oddRowColor } = this.props;
|
||||
let { columns } = this.state;
|
||||
let filteredPageItems = enablePagination ? this.paginateFn(filteredItems) : filteredItems;
|
||||
|
||||
return (
|
||||
<div className={styles.reactDatatable}>
|
||||
|
@ -201,104 +242,65 @@ export default class ReactDatatable extends React.Component<IReactDatatableProps
|
|||
description={strings.ConfigureWebpartDescription}
|
||||
buttonLabel={strings.ConfigureWebpartButtonLabel}
|
||||
hideButton={displayMode === DisplayMode.Read}
|
||||
onConfigure={this._onConfigure} /> : <>
|
||||
<WebPartTitle
|
||||
title={title}
|
||||
displayMode={DisplayMode.Read}
|
||||
updateProperty={() => { }}>
|
||||
</WebPartTitle>
|
||||
{ list && fields && fields.length ?
|
||||
<div>
|
||||
<Grid container className={styles.dataTableUtilities}>
|
||||
<Grid item xs={6} className={styles.downloadButtons}>
|
||||
{
|
||||
enableDownloadAsCsv
|
||||
onConfigure={this._onConfigure} /> : <><>
|
||||
<WebPartTitle
|
||||
title={title}
|
||||
displayMode={DisplayMode.Read}
|
||||
updateProperty={() => { }}>
|
||||
</WebPartTitle>
|
||||
{list && fields && fields.length ?
|
||||
<div>
|
||||
<Grid container className={styles.dataTableUtilities}>
|
||||
<Grid item xs={6} className={styles.downloadButtons}>
|
||||
{enableDownloadAsCsv
|
||||
? <ExportListItemsToCSV
|
||||
columnHeader={columns.map(c => c.headerName)}
|
||||
columnHeader={columns.map(c => c.name)}
|
||||
listName={list}
|
||||
description={title}
|
||||
listItems={listItems}
|
||||
/> : <></>
|
||||
}
|
||||
{
|
||||
enableDownloadAsPdf
|
||||
dataSource={()=> this.exportDataFormatter(fields, filteredItems, csvCellFormatter)}
|
||||
/> : <></>}
|
||||
{enableDownloadAsPdf
|
||||
? <ExportListItemsToPDF
|
||||
listName={list}
|
||||
htmlElementForPDF='#dataTable'
|
||||
/>
|
||||
: <></>
|
||||
}
|
||||
</Grid>
|
||||
<Grid container justify='flex-end' xs={6}>
|
||||
{
|
||||
enableSearching ?
|
||||
title={title}
|
||||
columns={columns.map(c => c.name)}
|
||||
oddRowColor={oddRowColor}
|
||||
evenRowColor={evenRowColor}
|
||||
dataSource={()=> this.exportDataFormatter(fields, filteredItems, pdfCellFormatter)} />
|
||||
: <></>}
|
||||
</Grid>
|
||||
<Grid container justify='flex-end' xs={6}>
|
||||
{enableSearching ?
|
||||
<TextField
|
||||
onChange={this.handleSearch.bind(this)}
|
||||
size="small"
|
||||
label="Search"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
: <></>
|
||||
}
|
||||
placeholder="Search"
|
||||
className={styles.txtSearchBox} />
|
||||
: <></>}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div id="generateTable">
|
||||
<TableContainer component={Paper} >
|
||||
<Table aria-label="customized table" id="dataTable" >
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((c) => (
|
||||
<StyledTableCell key={c.headerName}>
|
||||
{
|
||||
(sortBy && sortBy.indexOf(c.field) !== -1)
|
||||
? <TableSortLabel
|
||||
active={sortingFields === c.field}
|
||||
direction={sortingFields === c.field ? sortDirection : 'asc'}
|
||||
onClick={this.handleSorting(c.field)}
|
||||
>
|
||||
{c.headerName}
|
||||
</TableSortLabel>
|
||||
: c.headerName
|
||||
}
|
||||
</StyledTableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredItems.map((row, index) => (
|
||||
<TableRow
|
||||
style={{ backgroundColor: ((index + 1) % 2 === 0) ? evenRowColor : oddRowColor }} >
|
||||
{columns.map((c) => (
|
||||
<StyledTableCell >{row[c.field]}</StyledTableCell >
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
{enablePagination ?
|
||||
<React.Fragment>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<Pagination
|
||||
colSpan={columns.length}
|
||||
onPaginationUpdate={this.handlePaginationChange.bind(this)}
|
||||
totalItems={listItems.length} />
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</React.Fragment> : <></>
|
||||
}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div> : <Alert severity="info">
|
||||
{strings.ListFieldValidation}</Alert>
|
||||
}</>
|
||||
<div id="generateTable">
|
||||
<DetailsList
|
||||
items={filteredPageItems}
|
||||
columns={columns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
isHeaderVisible={true}
|
||||
onRenderRow={this._onRenderRow}
|
||||
/>
|
||||
<div>
|
||||
{this.props.enablePagination ?
|
||||
<Pagination
|
||||
currentPage={this.state.page}
|
||||
totalItems={filteredItems.length}
|
||||
onChange={this.handlePaginationChange.bind(this)}
|
||||
/>
|
||||
: <></>}
|
||||
</div>
|
||||
</div>
|
||||
</div> : <MessageBar>
|
||||
{strings.ListFieldValidation}
|
||||
</MessageBar>}</>
|
||||
</>
|
||||
}
|
||||
</div >
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@ define([], function () {
|
|||
"ConfigureWebpartDescription": "Please configure the web part.",
|
||||
"ListFieldValidation": "Please select the list fields.",
|
||||
"ListPickerLabel": "Select a list",
|
||||
"MultiSelectFieldLabel": "Multi select field",
|
||||
"MultiSelectFieldLabel": "Select list fields",
|
||||
"SortingToggleLabel": "Enable Sort",
|
||||
"SortByLabel": "Default Sort By",
|
||||
"SearchingToggleLabel": "Enable Search",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"downlevelIteration": true,
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
|
@ -25,7 +26,9 @@
|
|||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
"es2015",
|
||||
"es2015.collection",
|
||||
"ES2017"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
@ -36,4 +39,4 @@
|
|||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue