Added Grid layout and adjusted compact layout
This commit is contained in:
parent
800f21cd14
commit
15bea4986c
|
@ -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 |
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './GridLayout.types';
|
||||
export * from './GridLayout';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export interface IReactMyGroupsProps {
|
||||
description: string;
|
||||
title: string;
|
||||
layout: string;
|
||||
}
|
||||
|
|
|
@ -2,4 +2,5 @@ import * as MicrosoftGroup from '@microsoft/microsoft-graph-types';
|
|||
|
||||
export interface IReactMyGroupsState {
|
||||
groups: MicrosoftGroup.Group[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
<GroupList groups={this.state.groups} onRenderItem={(item: any, index: number) => this._onRenderItem(item, index)}/>
|
||||
<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,41 +68,46 @@ 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}
|
||||
/>
|
||||
</a>
|
||||
</DocumentCardDetails>
|
||||
</DocumentCard>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue