I'm new to SPFX / React Development, So I thought I'd start with a simple project!
I want to add a Resource Quick Access webpart on my Modern Sharepoint Page where users can quickly access SharePoint/OneDrive/Team files etc... I am using the MSGraphClient with UI Fabric
I'm getting this error
(property) BaseWebPart.properties: AlphaResources This property is the pointer to the custom property bag of the web part. @readonly No overload matches this call. The last overload gave the following error. Argument of type '{ description: any; context: WebPartContext; }' is not assignable to parameter of type 'Attributes'. Object literal may only specify known properties, and 'description' does not exist in type 'Attributes'.ts(2769) index.d.ts(238, 14): The last overload is declared here.
I really don't understand why, I haven't had this in previous test web parts
So I could really do with your help.
in the webpart.ts I have
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneLabel,
PropertyPaneToggle,
PropertyPaneDropdown,
PropertyPaneCheckbox,
PropertyPaneLink,
PropertyPaneSlider
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'AlphaResourcesWebPartStrings';
import AlphaResources from './components/AlphaResources';
import IAlphaResourcesProps from './components/AlphaResources';
export default class AlphaResourcesWebPart extends BaseClientSideWebPart<IAlphaResourcesProps> {
public render(): void {
const element: React.ReactElement<IAlphaResourcesProps> = React.createElement(
AlphaResources,
{
description: this.properties.description,
context: this.context
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}
in the AlphaResources.tsx (code based on example [https://developer.microsoft.com/en-us/fabric#/controls/web/detailslist][1])
import * as React from 'react';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { MSGraphClient } from '@microsoft/sp-http';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Fabric } from 'office-ui-fabric-react/lib/Fabric';
import { Announced } from 'office-ui-fabric-react/lib/Announced';
import { DetailsList, DetailsListLayoutMode, Selection, SelectionMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot';
import { Nav, INavLink } from 'office-ui-fabric-react/lib/Nav';
import { Link } from 'office-ui-fabric-react/lib/Link';
const classNames = mergeStyleSets({
fileIconHeaderIcon: {
padding: 0,
fontSize: '16px'
},
fileIconCell: {
textAlign: 'center',
selectors: {
'&:before': {
content: '.',
display: 'inline-block',
verticalAlign: 'middle',
height: '100%',
width: '0px',
visibility: 'hidden'
}
}
},
fileIconImg: {
verticalAlign: 'middle',
maxHeight: '16px',
maxWidth: '16px'
},
controlWrapper: {
display: 'flex',
flexWrap: 'wrap'
},
exampleToggle: {
display: 'inline-block',
marginBottom: '10px',
marginRight: '30px'
},
selectionDetails: {
marginBottom: '20px'
}
});
const controlStyles = {
root: {
margin: '0 30px 20px 0',
maxWidth: '300px'
}
};
export interface IAlphaResourcesProps {
description: string;
context: WebPartContext;
}
export interface IAlphaResourcesState {
columns: IColumn[];
items: IDocument[];
selectionDetails: string;
isModalSelection: boolean;
isCompactMode: boolean;
announcedMessage?: string;
}
export interface IDocument {
key: string;
name: string;
value: string;
iconName: string;
webUrl:string;
fileType: string;
modifiedBy: string;
dateModified: string;
dateModifiedValue: number;
fileSize: string;
fileSizeRaw: number;
contentType:string;
parentId:string;
parentDriveId:string;
}
export default class AlphaResources extends React.Component<{}, IAlphaResourcesState> {
private _selection: Selection;
private _allItems: IDocument[];
constructor(props: {}) {
super(props);
var default_dir_id = "dir-id";
var default_drive_id = 'drive-id';
this._allItems = _getDocuments();
const columns: IColumn[] = [
{
key: 'column1',
name: 'File Type',
className: classNames.fileIconCell,
iconClassName: classNames.fileIconHeaderIcon,
ariaLabel: 'Column operations for File type, Press to sort on File type',
iconName: 'Page',
isIconOnly: true,
fieldName: 'name',
minWidth: 16,
maxWidth: 16,
onColumnClick: this._onColumnClick,
onRender: (item: IDocument) => {
return <img src={item.iconName} className={classNames.fileIconImg} alt={item.fileType + ' file icon'} />;
}
},
{
key: 'column2',
name: 'Name',
fieldName: 'name',
minWidth: 210,
maxWidth: 350,
isRowHeader: true,
isResizable: true,
isSorted: true,
isSortedDescending: false,
sortAscendingAriaLabel: 'Sorted A to Z',
sortDescendingAriaLabel: 'Sorted Z to A',
onColumnClick: this._onColumnClick,
data: 'string',
isPadded: true
},
{
key: 'column3',
name: 'Date Modified',
fieldName: 'dateModifiedValue',
minWidth: 70,
maxWidth: 90,
isResizable: true,
onColumnClick: this._onColumnClick,
data: 'number',
onRender: (item: IDocument) => {
return <span>{item.dateModified}</span>;
},
isPadded: true
},
{
key: 'column4',
name: 'Modified By',
fieldName: 'modifiedBy',
minWidth: 70,
maxWidth: 90,
isResizable: true,
isCollapsible: true,
data: 'string',
onColumnClick: this._onColumnClick,
onRender: (item: IDocument) => {
return <span>{item.modifiedBy}</span>;
},
isPadded: true
},
{
key: 'column5',
name: 'File Size',
fieldName: 'fileSizeRaw',
minWidth: 70,
maxWidth: 90,
isResizable: true,
isCollapsible: true,
data: 'number',
onColumnClick: this._onColumnClick,
onRender: (item: IDocument) => {
return <span>{item.fileSize}</span>;
}
}
];
this._selection = new Selection({
onSelectionChanged: () => {
this.setState({
selectionDetails: this._getSelectionDetails()
});
}
});
this.state = {
items: this._allItems,
columns: columns,
selectionDetails: this._getSelectionDetails(),
isModalSelection: false,
isCompactMode: false,
announcedMessage: undefined,
};
}
public render() : JSX.Element {
const {columns, isCompactMode, items, selectionDetails, isModalSelection, announcedMessage } = this.state;
return (
<Fabric>
<Pivot onLinkClick={this.onLinkClick}>
<PivotItem headerText="Alpha Resources">
<div className={classNames.controlWrapper}>
<TextField label="Filter by name:" onChange={this._onChangeText} styles={controlStyles} />
<Announced message={`Number of items after filter applied: ${items.length}.`} />
</div>
<DetailsList
items={items}
compact={isCompactMode}
columns={columns}
selectionMode={SelectionMode.none}
getKey={this._getKey}
setKey="none"
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}
onItemInvoked={this._onItemInvoked}
/>
</PivotItem>
<PivotItem headerText="My OneDrive">
<Nav
onLinkClick={this.onNavClick}
styles={{
root: {
width: 208,
height: 350,
boxSizing: 'border-box',
border: '1px solid #eee',
overflowY: 'auto'
}
}}
groups={[
{
links: [
{
name: 'My Files',
url:'#',
key: 'key1',
},
{
name: 'Shared',
url: '#',
key: 'key2',
},
{
name: 'Recent',
url: '#',
key: 'key3',
}
]
}
]}
/>
<div className={classNames.controlWrapper}>
<TextField label="Filter by name:" onChange={this._onChangeText} styles={controlStyles} />
<Announced message={`Number of items after filter applied: ${items.length}.`} />
</div>
<DetailsList
items={items}
compact={isCompactMode}
columns={columns}
selectionMode={SelectionMode.none}
getKey={this._getKey}
setKey="none"
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}
onItemInvoked={this._onItemInvoked}
/>
</PivotItem>
<PivotItem headerText="Team Resources">
<div className={classNames.controlWrapper}>
<TextField label="Filter by name:" onChange={this._onChangeText} styles={controlStyles} />
<Announced message={`Number of items after filter applied: ${items.length}.`} />
</div>
<DetailsList
items={items}
compact={isCompactMode}
columns={columns}
selectionMode={SelectionMode.none}
getKey={this._getKey}
setKey="none"
layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true}
onItemInvoked={this._onItemInvoked}
/>
</PivotItem>
</Pivot>
</Fabric>
);
}
public onLinkClick(item: PivotItem): void {
alert(item.props.headerText);
}
public onNavClick(ev: React.MouseEvent<HTMLElement>, item?: INavLink) {
if (item) {
alert(item.name);
}
}
public componentDidUpdate(previousProps: any, previousState: IAlphaResourcesState) {
if (previousState.isModalSelection !== this.state.isModalSelection && !this.state.isModalSelection) {
this._selection.setAllSelected(false);
}
}
private _getKey(item: any, index?: number): string {
return item.key;
}
private _onChangeCompactMode = (ev: React.MouseEvent<HTMLElement>, checked: boolean): void => {
this.setState({ isCompactMode: checked });
};
private _onChangeModalSelection = (ev: React.MouseEvent<HTMLElement>, checked: boolean): void => {
this.setState({ isModalSelection: checked });
};
private _onChangeText = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text: string): void => {
this.setState({
items: text ? this._allItems.filter(i => i.name.toLowerCase().indexOf(text) > -1) : this._allItems
});
};
private _onItemInvoked(item: any): void {
alert(`Item invoked: ${item.name}`);
}
private _getSelectionDetails(): string {
const selectionCount = this._selection.getSelectedCount();
switch (selectionCount) {
case 0:
return 'No items selected';
case 1:
return '1 item selected: ' + (this._selection.getSelection()[0] as IDocument).name;
default:
return `${selectionCount} items selected`;
}
}
private _onColumnClick = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
const { columns, items } = this.state;
const newColumns: IColumn[] = columns.slice();
const currColumn: IColumn = newColumns.filter(currCol => column.key === currCol.key)[0];
newColumns.forEach((newCol: IColumn) => {
if (newCol === currColumn) {
currColumn.isSortedDescending = !currColumn.isSortedDescending;
currColumn.isSorted = true;
this.setState({
announcedMessage: `${currColumn.name} is sorted ${currColumn.isSortedDescending ? 'descending' : 'ascending'}`
});
} else {
newCol.isSorted = false;
newCol.isSortedDescending = true;
}
});
const newItems = _copyAndSort(items, currColumn.fieldName!, currColumn.isSortedDescending);
this.setState({
columns: newColumns,
items: newItems
});
};
}
function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
const key = columnKey as keyof T;
return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
}
function _getDocuments(drive_type: any, dir_id: any, drive_id: any, action: any, item_id: any) {
let graphUrl :string;
const items: IDocument[] = [];
if (drive_type == 'me' && !dir_id) {
graphUrl = "/me/drive/root/children";
}
if (drive_type == 'shared' && !dir_id) {
graphUrl = "/me/drive/sharedWithMe";
}
if (drive_type == 'recent' && !dir_id) {
graphUrl = "/me/drive/recent";
}
if (drive_type == 'me' && dir_id) {
graphUrl = "/me/" + dir_id + "/drive/root/children";
}
if (drive_type == 'shared' && dir_id && drive_id) {
graphUrl = "/drives/" + drive_id + "/items/" + dir_id + "/children";
}
if (drive_type == 'recent' && dir_id) {
graphUrl = "/drives/" + dir_id;
}
if (drive_type == 'groups' || drive_type == 'teams' && dir_id) {
graphUrl = "/groups/" + dir_id + "/drive/root/children";
}
//trigger if download required
if(drive_type == 'me' && action=='download' && item_id){
graphUrl="/me/drive/items/"+item_id+"/content";
}
//console.log(graphUrl)
this.props.context.msGraphClientFactory
.getClient()
.then((client: MSGraphClient): void => {
client
.api(graphUrl)
.version("v1.0")
.get((error, response: any, rawResponse?: any) => {
if (error) {
console.error(error);
return;
}
if (response) {
//console.log(response);
//var driveItems: Array<IDocument> = new Array<IDocument>();
var contentType;
response.value.map((item: any) => {
if(item.folder){
contentType = 'folder';
}else{
contentType = 'file';
}
items.push({
key:item.id,
name:item.name,
value:item.name,
iconName:'',
fileType: '',
webUrl:item.webUrl,
fileSize: item.size,
fileSizeRaw: item.size,
dateModified: item.lastModifiedDateTime,
modifiedBy: item.lastModifiedBy.user.displayName,
dateModifiedValue: item.lastModifiedDateTime,
contentType:contentType,
parentId:item.parentReference.id,
parentDriveId:item.parentReference.driveId,
});
});
return items
// Update the component state accordingly to the result
//console.log(items);
}
});
});
}