Added Grid layout and adjusted compact layout

This commit is contained in:
zachroberts8668 2020-07-08 22:07:51 -04:00
parent 800f21cd14
commit 15bea4986c
15 changed files with 402 additions and 42 deletions

View File

@ -19,7 +19,14 @@ extensions:
Using Microsoft Graph, this webpart grabs the Office 365 groups the current user is a member of with links to the groups SharePoint site.
![Demo](./assets/example.png)
The webpart has been updated to include a grid like in addition to the compact layout as seen below:
![Grid Demo](./assets/React-MyGroups_Grid.png)
Compact Layout:
![Compact Demo](./assets/React-MyGroups_Compact.png)
You can change between the grid and compact layout through the settings in the property pane:
![Property Pane Demo](./assets/React-MyGroups_Property.png)
## Used SharePoint Framework Version
@ -42,6 +49,7 @@ Version|Date|Comments
-------|----|--------
1.0|September 13, 2019|Initial release
1.1|June 1, 2020| Updated to SPFX 1.10.0
1.2|July 8, 2020| Added Grid Layout
## Disclaimer

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -2,13 +2,14 @@ import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { IPropertyPaneConfiguration, PropertyPaneTextField } from "@microsoft/sp-property-pane";
import { IPropertyPaneConfiguration, PropertyPaneTextField, PropertyPaneChoiceGroup } from "@microsoft/sp-property-pane";
import GroupService from '../../services/GroupService';
import * as strings from 'ReactMyGroupsWebPartStrings';
import { ReactMyGroups, IReactMyGroupsProps } from './components';
export interface IReactMyGroupsWebPartProps {
description: string;
title: string;
layout: string;
}
export default class ReactMyGroupsWebPart extends BaseClientSideWebPart<IReactMyGroupsWebPartProps> {
@ -17,7 +18,8 @@ export default class ReactMyGroupsWebPart extends BaseClientSideWebPart<IReactMy
const element: React.ReactElement<IReactMyGroupsProps > = React.createElement(
ReactMyGroups,
{
description: this.properties.description
title: this.properties.title,
layout: this.properties.layout
}
);
@ -39,6 +41,7 @@ export default class ReactMyGroupsWebPart extends BaseClientSideWebPart<IReactMy
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
const { layout } = this.properties;
return {
pages: [
{
@ -49,8 +52,26 @@ export default class ReactMyGroupsWebPart extends BaseClientSideWebPart<IReactMy
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
PropertyPaneTextField('title', {
label: 'Title'
}),
PropertyPaneChoiceGroup("layout", {
label: 'Layout Option',
options: [
{
key: "Grid",
text: "Grid",
iconProps: { officeFabricIconFontName: "GridViewSmall"},
checked: layout === "Grid" ? true : false,
},
{
key: "Compact",
text: "Compact",
iconProps: { officeFabricIconFontName: "BulletedList2"},
checked: layout === "Compact" ? true : false
}
]
})
]
}

View File

@ -0,0 +1,60 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
:export {
padding: 20;
minWidth: 210;
maxWidth: 320;
compactThreshold: 480;
rowsPerPage: 3;
}
.gridLayout {
overflow: hidden;
font-size: 0;
position: relative;
background-color: transparent;
:global(.ms-DocumentCard) {
position: relative;
background-color: $ms-color-white;
height: 100%;
&:global(.ms-DocumentCard--compact) {
:global(.ms-DocumentCardPreview) {
-ms-flex-negative: 0;
flex-shrink: 0;
width: 144px;
}
}
:global(.ms-DocumentCardPreview-icon) img {
width: 32px;
height: 32px;
}
}
:global(.ms-DocumentCard:not(.ms-DocumentCard--compact)) {
min-width: 212px;
max-width: 286px;
:global(.ms-DocumentCardActivity) {
padding-bottom: 16px;
}
:global(.ms-DocumentCardTile-titleArea) {
height: 81px;
}
:global(.ms-DocumentCardLocation) {
padding: 12px 16px 5px 16px;
overflow: hidden;
text-overflow: ellipsis;
}
}
:global(.ms-List-cell) {
vertical-align: top;
display: inline-block;
margin-bottom: 20px;
}
}

View File

@ -0,0 +1,89 @@
import * as React from 'react';
import styles from './GridLayout.module.scss';
// Used to render list grid
import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone';
import { List } from 'office-ui-fabric-react/lib/List';
import { IRectangle, ISize } from 'office-ui-fabric-react/lib/Utilities';
import { Spinner } from 'office-ui-fabric-react';
import { IGridLayoutProps, IGridLayoutState } from './GridLayout.types';
const ROWS_PER_PAGE: number = +styles.rowsPerPage;
const MAX_ROW_HEIGHT: number = +styles.maxWidth;
const PADDING: number = +styles.padding;
const MIN_WIDTH: number = +styles.minWidth;
const COMPACT_THRESHOLD: number = +styles.compactThreshold;
export class GridLayout extends React.Component<IGridLayoutProps, IGridLayoutState> {
constructor(props: IGridLayoutProps) {
super(props);
this.state = {
isLoading: true
};
}
private _columnCount: number;
private _columnWidth: number;
private _rowHeight: number;
private _isCompact: boolean;
public render(): React.ReactElement<IGridLayoutProps> {
return (
<div role="group" aria-label={this.props.ariaLabel}>
<FocusZone>
<List
role="presentation"
className={styles.gridLayout}
items={this.props.items}
getItemCountForPage={this._getItemCountForPage}
getPageHeight={this._getPageHeight}
onRenderCell={this._onRenderCell}
{...this.props.listProps}
/>
</FocusZone>
</div>
);
}
public componentDidMount = (): void => {
}
private _getItemCountForPage = (itemIndex: number, surfaceRect: IRectangle): number => {
if (itemIndex === 0) {
this._isCompact = surfaceRect.width < COMPACT_THRESHOLD;
if (this._isCompact) {
this._columnCount = 1;
this._columnWidth = surfaceRect.width;
} else {
this._columnCount = Math.ceil(surfaceRect.width / (MAX_ROW_HEIGHT));
this._columnWidth = Math.max(MIN_WIDTH, Math.floor(surfaceRect.width / this._columnCount) + Math.floor(PADDING / this._columnCount));
this._rowHeight = this._columnWidth;
}
}
return this._columnCount * ROWS_PER_PAGE;
}
private _getPageHeight = (): number => {
return this._rowHeight * ROWS_PER_PAGE;
}
private _onRenderCell = (item: any, index: number | undefined): JSX.Element => {
console.log(item.displayName);
const isCompact: boolean = this._isCompact;
const cellPadding: number = index % this._columnCount !== this._columnCount - 1 && !isCompact ? PADDING : 0;
const finalSize: ISize = { width: this._columnWidth, height: this._rowHeight };
const cellWidth: number = isCompact ? this._columnWidth + PADDING : this._columnWidth - PADDING;
return (
<div
style={{
width: `${cellWidth}px`,
marginRight: `${cellPadding}px`
}}
>
{this.props.onRenderGridItem(item, finalSize, isCompact)}
</div>
);
}
}

View File

@ -0,0 +1,28 @@
import { ISize } from 'office-ui-fabric-react/lib/Utilities';
import { IListProps } from 'office-ui-fabric-react/lib/List';
export interface IGridLayoutProps {
/**
* Accessible text for the grid layout
*/
ariaLabel?: string;
/**
* The array of items to display
*/
items: any[];
/**
* In case you want to override the underlying list
*/
listProps?: Partial<IListProps>;
/**
* The method to render each cell item
*/
onRenderGridItem: (item: any, finalSize: ISize, isCompact: boolean) => JSX.Element;
}
export interface IGridLayoutState {
isLoading?: boolean;
}

View File

@ -0,0 +1,2 @@
export * from './GridLayout.types';
export * from './GridLayout';

View File

@ -1,12 +1,14 @@
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
:export {
padding: 20;
padding: 5;
minWidth: 210;
maxWidth: 320;
rowsPerPage: 3;
}
.compactLayout {
overflow: hidden;
font-size: 0;
@ -44,6 +46,5 @@
:global(.ms-List-cell) {
vertical-align: top;
display: inline-block;
margin-bottom: 20px;
}
}

View File

@ -1,3 +1,4 @@
export interface IReactMyGroupsProps {
description: string;
title: string;
layout: string;
}

View File

@ -2,4 +2,5 @@ import * as MicrosoftGroup from '@microsoft/microsoft-graph-types';
export interface IReactMyGroupsState {
groups: MicrosoftGroup.Group[];
isLoading: boolean;
}

View File

@ -2,6 +2,131 @@
.reactMyGroups {
.compactContainer {
margin: 0px 5px 5px;
position: relative;
display: inline-block;
vertical-align: top;
}
.compactA {
outline: 0;
text-decoration: none;
display: block;
margin-top: 4px;
width: 251.333px;
border: 1px solid transparent;
margin-bottom: 8px;
}
.compactWrapper {
display: flex;
align-items: center;
position: relative;
}
.compactBanner {
margin-right: 12px;
width: 32px;
height: 32px;
flex-shrink: 0;
}
.compactDetails {
overflow: hidden;
display: flex;
flex-direction: column;
}
.compactTitle {
font-size: 14px;
font-weight: 600;
margin: 6px 0;
text-overflow: ellipsis;
white-space: nowrap;
color: rgb(50,49,48);
display: block;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
}
.cardContainer{
outline: transparent;
position: relative;
}
.siteCard {
height: 200px;
max-width: 286px;
box-sizing: border-box;
margin-bottom: 20px;
outline: transparent;
position: relative;
background-color: rgb(255, 255, 255);
box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
border-width: 1px;
border-style: solid;
border-image: initial;
border-radius: 2px;
overflow: hidden;
border-color: rgb(237, 235, 233);
}
.cardBanner {
position: relative;
height: 144px;
padding-top: 40px;
width: 100%;
}
.cardTitle {
font-size: 16px;
margin-top: 28px;
font-weight: 600;
line-height: 20px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
box-sizing: content-box;
max-height: 2.7em;
overflow: hidden;
text-align: left;
padding: 0 12px;
color: rgb(50, 49, 48);
}
.topBanner {
position: absolute;
top: 0;
height: 40px;
width: 100%;;
}
.bannerImg {
position: absolute;
top: 12px;
left: 12px;
height: 48px;
width: 48px;
box-sizing: border-box;
box-shadow: 0 0 4px 0 rgba(0,0,0,.2);
border-width: 1px;
border-style: solid;
border-image: initial;
border-color: rgb(255,255,255);
background-position: 50%;
background-repeat: no-repeat;
background-size: contain;
}
.docTile {
background-color: transparent;
outline: transparent;
position: relative;
}
a {
text-decoration: none;
}
@ -28,8 +153,16 @@
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
text-align: left;
color: rgb(50, 49, 48);
white-space: pre-wrap;
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 20px;
font-weight: 600;
margin-right: 32px;
margin-bottom: 18px;
flex-grow: 1;
}
.subTitle {

View File

@ -6,7 +6,10 @@ import GroupService from '../../../../services/GroupService';
import { IReactMyGroupsState } from './IReactMyGroupsState';
import { GroupList } from '../GroupList';
import { IGroup } from '../../../../models';
import { DocumentCard, DocumentCardType, DocumentCardDetails, DocumentCardTitle, IDocumentCardPreviewProps, ImageFit, DocumentCardPreview } from 'office-ui-fabric-react';
import { Spinner, DocumentCard, DocumentCardType, DocumentCardDetails, DocumentCardTitle, IDocumentCardPreviewProps, ImageFit, DocumentCardPreview, IconFontSizes, ISize, isVirtualElement, DocumentCardLocation, DocumentCardActivity } from 'office-ui-fabric-react';
import { GridLayout } from '../GridList';
const colors = ['#17717A','#4A69DB','#303952','#A4262C','#3A96DD','#CA5010','#8764B8','#498205','#69797E'];
export class ReactMyGroups extends React.Component<IReactMyGroupsProps, IReactMyGroupsState> {
@ -14,7 +17,8 @@ export class ReactMyGroups extends React.Component<IReactMyGroupsProps, IReactMy
super(props);
this.state = {
groups: []
groups: [],
isLoading: true
};
}
@ -22,8 +26,17 @@ export class ReactMyGroups extends React.Component<IReactMyGroupsProps, IReactMy
public render(): React.ReactElement<IReactMyGroupsProps> {
return (
<div className={ styles.reactMyGroups }>
<h1>My Office 365 Groups</h1>
<div className={styles.title} role="heading" aria-level={2}>{this.props.title} </div>
{this.state.isLoading ?
<Spinner label="Loading sites..." />
:
<div>
{this.props.layout == 'Compact' ?
<GroupList groups={this.state.groups} onRenderItem={(item: any, index: number) => this._onRenderItem(item, index)}/>
: <GridLayout items={this.state.groups} onRenderGridItem={(item: any, finalSize: ISize, isCompact: boolean) => this._onRenderGridItem(item, finalSize, isCompact)}/>
}
</div>
}
</div>
);
}
@ -34,7 +47,6 @@ export class ReactMyGroups extends React.Component<IReactMyGroupsProps, IReactMy
public _getGroups = (): void => {
GroupService.getGroups().then(groups => {
// console.log(groups);
this.setState({
groups: groups
});
@ -45,7 +57,6 @@ export class ReactMyGroups extends React.Component<IReactMyGroupsProps, IReactMy
public _getGroupLinks = (groups: any): void => {
groups.map(groupItem => (
GroupService.getGroupLinks(groupItem).then(groupurl => {
// console.log(groupurl.value);
this.setState(prevState => ({
groups: prevState.groups.map(group => group.id === groupItem.id ? {...group, url: groupurl.value} : group)
}));
@ -57,39 +68,44 @@ export class ReactMyGroups extends React.Component<IReactMyGroupsProps, IReactMy
public _getGroupThumbnails = (groups: any): void => {
groups.map(groupItem => (
GroupService.getGroupThumbnails(groupItem).then(grouptb => {
console.log(grouptb);
//set group color:
const itemColor = colors[Math.floor(Math.random() * colors.length)];
this.setState(prevState => ({
groups: prevState.groups.map(group => group.id === groupItem.id ? {...group, thumbnail: grouptb} : group)
groups: prevState.groups.map(group => group.id === groupItem.id ? {...group, thumbnail: grouptb, color: itemColor} : group)
}));
})
));
console.log('Set False');
this.setState({
isLoading: false
});
}
private _onRenderItem = (item: any, index: number): JSX.Element => {
const previewProps: IDocumentCardPreviewProps = {
previewImages: [
{
previewImageSrc: item.thumbnail,
imageFit: ImageFit.center,
height: 48,
width: 48
}
]
};
return (
<div>
<DocumentCard
type={DocumentCardType.compact}
>
<DocumentCardPreview {...previewProps} />
<DocumentCardDetails>
<a href={item.url}>
<DocumentCardTitle
title={item.displayName}
/>
<div className={styles.compactContainer}>
<a className={styles.compactA} href={item.url}>
<div className={styles.compactWrapper}>
<img className={styles.compactBanner} src={item.thumbnail} />
<div className={styles.compactDetails}>
<div className={styles.compactTitle}>{item.displayName}</div>
</div>
</div>
</a>
</div>
);
}
private _onRenderGridItem = (item: any, finalSize: ISize, isCompact: boolean): JSX.Element => {
return (
<div className={styles.siteCard}>
<a href={item.url}>
<div className={styles.cardBanner}>
<div className={styles.topBanner} style={{backgroundColor: item.color}}></div>
<img className={styles.bannerImg} src={item.thumbnail} />
<div className={styles.cardTitle}>{item.displayName}</div>
</div>
</a>
</DocumentCardDetails>
</DocumentCard>
</div>
);
}