added drag and drop follow sites webpart
This commit is contained in:
parent
5ebaaa252e
commit
d6b14a440b
|
@ -2,37 +2,48 @@
|
|||
|
||||
## Summary
|
||||
|
||||
Short summary on functionality and used technologies.
|
||||
![image](./assets/sorting.gif)
|
||||
|
||||
[picture of the solution in action, if possible]
|
||||
This webpart is a good example (starting point) for a solution to implement alternative view for user followed sites (or any kind of links). The webpart uses Microsoft Graph so it presents how to define needed web api permissions in package-solution and use MS Graph API endpoints.
|
||||
|
||||
![image](./assets/mainImage.png)
|
||||
how it looks on SP site
|
||||
|
||||
![image](./assets/appInTeams.png)
|
||||
how it looks in Teams
|
||||
|
||||
![image](./assets/nothingToFollow.png)
|
||||
when user does not have any followed sites
|
||||
|
||||
Another cool feature is also done using MS Graph in order to save or update the order of the links as a .json file in special approot folder which is kept on each individual user OneDrive. Thanks to that the webpart may keep the user order of the links in one place where it may be easily used in a SharePoint page or in Teams.
|
||||
![image](./assets/linkSavedInJsonFile.png)
|
||||
![image](./assets/dataAsJson.png)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![version](https://img.shields.io/badge/version-1.13-green.svg)
|
||||
![SPFx 1.13.0](https://img.shields.io/badge/SPFx-1.13-green.svg)
|
||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
- [SharePoint Framework](https://aka.ms/spfx)
|
||||
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||
|
||||
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
> Any special pre-requisites?
|
||||
As this solution uses Microsoft Graph to get user followed sites it is required to approve all web api permission requests in SharePoint Admin page https://YourCoolTenantNameHere-admin.sharepoint.com/_layouts/15/online/AdminHome.aspx#/webApiPermissionManagement
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
folder name | Author details (name, company, twitter alias with link)
|
||||
react-followed-drag-and-drop-grid | [Adam Wójcik](https://github.com/Adam-it)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.1|March 10, 2021|Update comment
|
||||
1.0|January 29, 2021|Initial release
|
||||
1.0|January 09, 2022|Initial release
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
@ -45,10 +56,12 @@ Version|Date|Comments
|
|||
- Clone this repository
|
||||
- Ensure that you are at the solution folder
|
||||
- in the command-line run:
|
||||
- **npm install**
|
||||
- **gulp serve**
|
||||
|
||||
> Include any additional steps as needed.
|
||||
- `npm install`
|
||||
- `gulp serve`
|
||||
- `gulp bundle --ship`
|
||||
- `gulp package-solution --ship`
|
||||
- Add to AppCatalog and deploy
|
||||
- Approve the MS Graph API permissions in SharePoint Admin page
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -56,13 +69,9 @@ Description of the extension that expands upon high-level summary above.
|
|||
|
||||
This extension illustrates the following concepts:
|
||||
|
||||
- topic 1
|
||||
- topic 2
|
||||
- topic 3
|
||||
|
||||
> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance.
|
||||
|
||||
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
|
||||
- how to use Microsoft Graph api and get user followed sites or use special approot on user OneDrive
|
||||
- how to save/update data in a json file special approot folder on user OneDrive as a place to keep data a use in SharePoint or Teams
|
||||
- how to create a simple alternative drag and drop view for links
|
||||
|
||||
## References
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 509 KiB |
|
@ -13,6 +13,7 @@
|
|||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"DragAndDropFollowedSitesWebPartStrings": "lib/webparts/dragAndDropFollowedSites/loc/{locale}.js"
|
||||
"DragAndDropFollowedSitesWebPartStrings": "lib/webparts/dragAndDropFollowedSites/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": true,
|
||||
"isDomainIsolated": false,
|
||||
"developer": {
|
||||
"name": "",
|
||||
"websiteUrl": "",
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
|
||||
"initialPage": "https://tenanttocheck.sharepoint.com/sites/webpartfollowedSites/_layouts/workbench.aspx"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,14 +9,17 @@
|
|||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"office-ui-fabric-react": "7.174.1",
|
||||
"@microsoft/sp-core-library": "1.13.0",
|
||||
"@microsoft/sp-lodash-subset": "1.13.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.13.0",
|
||||
"@microsoft/sp-property-pane": "1.13.0",
|
||||
"@microsoft/sp-webpart-base": "1.13.0",
|
||||
"@microsoft/sp-lodash-subset": "1.13.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.13.0"
|
||||
"@pnp/spfx-controls-react": "^3.5.0",
|
||||
"classnames": "^2.2.6",
|
||||
"office-ui-fabric-react": "7.174.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-sortable-hoc": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "16.9.51",
|
||||
|
|
|
@ -2,14 +2,20 @@ import * as React from 'react';
|
|||
import * as ReactDom from 'react-dom';
|
||||
import * as strings from 'DragAndDropFollowedSitesWebPartStrings';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import { initializeIcons } from 'office-ui-fabric-react';
|
||||
import { IPropertyPaneConfiguration } from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
import DragAndDropFollowedSites from './components/DragAndDropFollowedSites';
|
||||
import { IDragAndDropFollowedSitesProps } from './components/IDragAndDropFollowedSitesProps';
|
||||
import DragAndDropFollowedSites from './components/dragAndDropFollowedSites/DragAndDropFollowedSites';
|
||||
import { IDragAndDropFollowedSitesProps } from './components/dragAndDropFollowedSites/IDragAndDropFollowedSitesProps';
|
||||
import { IDragAndDropFollowedSitesWebPartProps } from './IDragAndDropFollowedSitesWebPartProps';
|
||||
|
||||
export default class DragAndDropFollowedSitesWebPart extends BaseClientSideWebPart<IDragAndDropFollowedSitesWebPartProps> {
|
||||
|
||||
public onInit(): Promise<void> {
|
||||
initializeIcons();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IDragAndDropFollowedSitesProps> = React.createElement(
|
||||
DragAndDropFollowedSites,
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
|
@ -1,22 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { MSGraphClient } from '@microsoft/sp-http';
|
||||
import { IDragAndDropFollowedSitesProps } from './IDragAndDropFollowedSitesProps';
|
||||
|
||||
export default class DragAndDropFollowedSites extends React.Component<IDragAndDropFollowedSitesProps, {}> {
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.props.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient): void => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IDragAndDropFollowedSitesProps> {
|
||||
return (
|
||||
<div>
|
||||
<span>fallowed sites</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
@import "../../styles/Main.module.scss";
|
||||
|
||||
.followedSites {
|
||||
.grid {
|
||||
@include ms-Grid;
|
||||
|
||||
.row {
|
||||
@include ms-Grid-row;
|
||||
|
||||
.columnFullWidth {
|
||||
@include ms-Grid-col;
|
||||
@include ms-sm12;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@include ms-font-xl;
|
||||
}
|
||||
|
||||
.sortableContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.isSortingActive .sortableItem label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sortableItem {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
min-width: $tileSize;
|
||||
min-height: $tileSize;
|
||||
margin: $marginSize;
|
||||
background-color: $themePrimary;
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 4px;
|
||||
color: $white;
|
||||
display: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&.moveButton {
|
||||
right: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sortableInnerItem {
|
||||
display: block;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
height: $tileHeight;
|
||||
padding-top: $tilePadding;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
i {
|
||||
@include ms-font-su;
|
||||
display: block;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
|
||||
&.noIcon {
|
||||
margin-top: $additionalMarginWhenNoIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sortableItemDragging {
|
||||
box-shadow: 0 0 $marginSize $neutralPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
text-align: center;
|
||||
margin-top: $marginStep;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import * as React from 'react';
|
||||
import * as strings from 'DragAndDropFollowedSitesWebPartStrings';
|
||||
import { MSGraphClient } from '@microsoft/sp-http';
|
||||
import { IDragAndDropFollowedSitesProps } from './IDragAndDropFollowedSitesProps';
|
||||
import { IDragAndDropFollowedSitesState } from './IDragAndDropFollowedSitesState';
|
||||
import FollowedSitesService from '../../services/FollowedSites/FollowedSitesService';
|
||||
import MyDataService from '../../services/MyData/MyDataService';
|
||||
import IMyDataServiceInput from '../../services/MyData/IMyDataServiceInput';
|
||||
import styles from './DragAndDropFollowedSites.module.scss';
|
||||
import sortableStyles from '../sortableList/Sortable.module.scss';
|
||||
import Constants from '../../../model/Constants';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import ErrorPanel from '../errorPanel/ErrorPanel';
|
||||
import NoItems from '../noItems/NoItems';
|
||||
import SortableList from '../sortableList/SortableList';
|
||||
import { arrayMove } from 'react-sortable-hoc';
|
||||
import IAppData from '../../../model/IAppData';
|
||||
import IFollowedSite from '../../../model/IFollowedSite';
|
||||
|
||||
export default class DragAndDropFollowedSites extends React.Component<IDragAndDropFollowedSitesProps, IDragAndDropFollowedSitesState> {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
followedSitesService: null,
|
||||
myDataService: null,
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
sortingIsActive: false,
|
||||
urls: []
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.props.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient): void => {
|
||||
const followedSitesService: FollowedSitesService = new FollowedSitesService(client);
|
||||
const myDataServiceInput: IMyDataServiceInput = {
|
||||
mSGraphClient: client,
|
||||
httpClient: this.props.context.httpClient,
|
||||
appDataFolderName: Constants.appDataFolderName,
|
||||
appDataJsonFileName: Constants.appDataJsonFileName
|
||||
};
|
||||
const myDataService: MyDataService = new MyDataService(myDataServiceInput);
|
||||
|
||||
this.setState({
|
||||
followedSitesService,
|
||||
myDataService
|
||||
});
|
||||
|
||||
this.state.myDataService.checkIfAppDataFolderExists()
|
||||
.then(appDataFolderExists => {
|
||||
if (appDataFolderExists.isError) {
|
||||
this.setState({
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appDataFolderExists.folderExists) {
|
||||
this.state.myDataService
|
||||
.createAppDataFolder()
|
||||
.then(folderName => {
|
||||
if (folderName === null) {
|
||||
this.setState({
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.LoadData();
|
||||
});
|
||||
} else {
|
||||
this.LoadData();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IDragAndDropFollowedSitesProps> {
|
||||
|
||||
const {
|
||||
urls,
|
||||
isLoading,
|
||||
isError,
|
||||
sortingIsActive } = this.state;
|
||||
|
||||
const isEmpty: boolean = urls.length === 0 && !isLoading;
|
||||
|
||||
return (
|
||||
<div className={styles.followedSites}>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.columnFullWidth}>
|
||||
<Label className={styles.title}>{strings.Title}</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.columnFullWidth}>
|
||||
<div className={!isLoading || isError ? styles.hide : null}>
|
||||
<Spinner label={strings.Loading} ariaLive='assertive' labelPosition='right' />
|
||||
</div>
|
||||
<div className={!isError ? styles.hide : null}>
|
||||
<ErrorPanel />
|
||||
</div>
|
||||
<div className={isError ? styles.hide : null}>
|
||||
<div className={!isEmpty ? styles.hide : null}>
|
||||
<NoItems />
|
||||
</div>
|
||||
<div className={sortingIsActive ? styles.isSortingActive : null} >
|
||||
<SortableList
|
||||
items={urls}
|
||||
axis='xy'
|
||||
helperClass={sortableStyles.sortableItemDragging}
|
||||
onSortEnd={this.onSortEnd}
|
||||
onSortStart={this.onSortStart}
|
||||
useDragHandle={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private LoadData(): void {
|
||||
this.state.myDataService
|
||||
.getJsonAppDataFile()
|
||||
.then(appData => {
|
||||
if (appData === null) {
|
||||
this.setState({
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.state.followedSitesService.getMyFollowedSites().then(followedSites => {
|
||||
const followedUrls: IFollowedSite[] = followedSites.value.map(element => {
|
||||
return {
|
||||
name: element[Constants.nameFollowedSites],
|
||||
url: element[Constants.urlFollowedSites]
|
||||
} as IFollowedSite;
|
||||
});
|
||||
followedUrls.forEach(followedItem => {
|
||||
if (appData.userFollowedSites.map(item => item.url).indexOf(followedItem.url) === -1) {
|
||||
appData.userFollowedSites.push(followedItem);
|
||||
}
|
||||
});
|
||||
appData.userFollowedSites = appData.userFollowedSites.filter(followedItem => followedUrls.map(item => item.url).indexOf(followedItem.url) > -1);
|
||||
this.state.myDataService.createOrUpdateJsonDataFile(appData);
|
||||
this.setState({
|
||||
urls: appData.userFollowedSites,
|
||||
isLoading: false,
|
||||
isError: false
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private onSortEnd = ({ oldIndex, newIndex }): void => {
|
||||
const prevItems = this.state.urls;
|
||||
this.setState({
|
||||
urls: arrayMove(prevItems, oldIndex, newIndex),
|
||||
sortingIsActive: false
|
||||
});
|
||||
|
||||
const appData: IAppData = { userFollowedSites: this.state.urls };
|
||||
this.state.myDataService.createOrUpdateJsonDataFile(appData);
|
||||
}
|
||||
|
||||
private onSortStart = (): void => {
|
||||
this.setState({ sortingIsActive: true });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import IFollowedSite from "../../../model/IFollowedSite";
|
||||
import FollowedSitesService from "../../services/FollowedSites/FollowedSitesService";
|
||||
import MyDataService from "../../services/MyData/MyDataService";
|
||||
|
||||
export interface IDragAndDropFollowedSitesState {
|
||||
followedSitesService: FollowedSitesService;
|
||||
myDataService: MyDataService;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
sortingIsActive: boolean;
|
||||
urls: IFollowedSite[];
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
@import "../../styles/Main.module.scss";
|
||||
|
||||
.panel {
|
||||
@include ms-font-xl;
|
||||
margin-top: $marginStep;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
padding: 20px 10px 20px 10px;
|
||||
background-color: $neutralLight;
|
||||
color: $neutralSecondary;
|
||||
|
||||
i {
|
||||
@include ms-font-su;
|
||||
}
|
||||
|
||||
.refreshButtonRow {
|
||||
text-align: right;
|
||||
max-width: 80%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-right: $marginStep;
|
||||
margin-bottom: $marginStep;
|
||||
margin-top: $marginStep;
|
||||
}
|
||||
|
||||
.console {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
max-width: 80%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@include ms-font-s;
|
||||
text-align: left;
|
||||
padding: 5px 10px 5px 10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import * as React from 'react';
|
||||
import * as strings from 'DragAndDropFollowedSitesWebPartStrings';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||
import IErrorPanelProps from './IErrorPanelProps';
|
||||
import IErrorPanelState from './IErrorPanelState';
|
||||
import styles from './ErrorPanel.module.scss';
|
||||
|
||||
export default class ErrorPanel extends React.Component<IErrorPanelProps, IErrorPanelState> {
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div>
|
||||
<Icon iconName={'BugSolid'} />
|
||||
</div>
|
||||
<div>
|
||||
<label>{strings.ErrorText}</label>
|
||||
</div>
|
||||
<div className={styles.refreshButtonRow}>
|
||||
<PrimaryButton onClick={() => this.refresh()}>
|
||||
{strings.ErrorPanelRefresh}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
<div className={styles.console}>
|
||||
<p>{strings.ErrorCouldNotGetData}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export default interface IErrorPanelProps { }
|
|
@ -0,0 +1 @@
|
|||
export default interface IErrorPanelState { }
|
|
@ -0,0 +1 @@
|
|||
export default interface INoItemsProps { }
|
|
@ -0,0 +1 @@
|
|||
export default interface INoItemsState { }
|
|
@ -0,0 +1,14 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
@import "../../styles/Main.module.scss";
|
||||
|
||||
.panel {
|
||||
@include ms-font-xl;
|
||||
margin-top: $marginStep;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
padding: 20px 10px 20px 10px;
|
||||
background-color: $neutralLight;
|
||||
color: $neutralSecondary;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import * as strings from 'DragAndDropFollowedSitesWebPartStrings';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import INoItemsProps from './INoItemsProps';
|
||||
import INoItemsState from './INoItemsState';
|
||||
import styles from './NoItems.module.scss';
|
||||
|
||||
export default class NoItems extends React.Component<INoItemsProps, INoItemsState> {
|
||||
public render() {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div>
|
||||
<Icon iconName={'Sad'} />
|
||||
</div>
|
||||
<div>
|
||||
<label>{strings.NoItemsText}</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from 'react';
|
||||
import { SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import styles from '../sortableList/Sortable.module.scss';
|
||||
|
||||
export default SortableElement(({ item }) => {
|
||||
const DragHandle = SortableHandle(() =>
|
||||
<label className={styles.moveButton}>
|
||||
<Icon iconName={'Move'} />
|
||||
</label>);
|
||||
const tileItem = item;
|
||||
let displayName = tileItem.name;
|
||||
if (displayName.length >= 15) {
|
||||
displayName = `${displayName.substring(0, 13)}...`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.sortableItem}>
|
||||
<DragHandle />
|
||||
<a className={styles.sortableInnerItem} href={tileItem.url}>
|
||||
<span>{displayName}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
@import "../../styles/Main.module.scss";
|
||||
|
||||
.sortableContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.isSortingActive .sortableItem label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sortableItem {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
min-width: $tileSize;
|
||||
min-height: $tileSize;
|
||||
margin: $marginSize;
|
||||
background-color: $themePrimary;
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 4px;
|
||||
color: $white;
|
||||
display: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&.moveButton {
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sortableInnerItem {
|
||||
display: block;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
height: $tileHeight;
|
||||
padding-top: $tilePadding;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
i {
|
||||
@include ms-font-su;
|
||||
display: block;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
|
||||
&.noIcon {
|
||||
margin-top: $additionalMarginWhenNoIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sortableItemDragging {
|
||||
box-shadow: 0 0 $marginSize $neutralPrimary;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import { SortableContainer } from 'react-sortable-hoc';
|
||||
import styles from './Sortable.module.scss';
|
||||
import SortableItem from '../sortableItem/SortableItem';
|
||||
|
||||
export default SortableContainer(({ items }) => (
|
||||
<div className={styles.sortableContainer}>
|
||||
{items.map((item, index) => (
|
||||
<SortableItem
|
||||
key={index}
|
||||
index={index}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
));
|
|
@ -1,7 +1,11 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
"Title": "Followed Sites",
|
||||
"Loading": "Loading...",
|
||||
"ErrorText": "There seems to be some error. Please try to refresh the site.",
|
||||
"ErrorPanelRefresh": "Refresh",
|
||||
"ErrorCouldNotGetData": "> Error 🐞 - Could not get user data",
|
||||
"NoItemsText": "It seems you don't follow any sites 🧐"
|
||||
}
|
||||
});
|
|
@ -1,7 +1,11 @@
|
|||
declare interface IDragAndDropFollowedSitesWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
Title: string;
|
||||
Loading: string;
|
||||
ErrorText: string;
|
||||
ErrorPanelRefresh: string;
|
||||
ErrorCouldNotGetData: string;
|
||||
NoItemsText: string;
|
||||
}
|
||||
|
||||
declare module 'DragAndDropFollowedSitesWebPartStrings' {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MSGraphClient } from '@microsoft/sp-http';
|
||||
|
||||
export default class FallowedSitesService {
|
||||
export default class FollowedSitesService {
|
||||
|
||||
private graphClient: MSGraphClient = null;
|
||||
|
||||
|
@ -8,10 +8,10 @@ export default class FallowedSitesService {
|
|||
this.graphClient = graphClient;
|
||||
}
|
||||
|
||||
public async getMyFallowedSites(): Promise<any> {
|
||||
public async getMyFollowedSites(): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
this.graphClient
|
||||
.api('/me/followedSites')
|
||||
.api('/me/followedSites?$top=1000')
|
||||
.version('v1.0')
|
||||
.get((error, response: any, rawResponse?: any) => {
|
||||
if (error) {
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
MSGraphClient,
|
||||
HttpClient,
|
||||
HttpClientResponse } from '@microsoft/sp-http';
|
||||
HttpClientResponse
|
||||
} from '@microsoft/sp-http';
|
||||
import IAppData from '../../../model/IAppData';
|
||||
import IAppDataFolderExistsOutput from '../../../model/IAppDataFolderExistsOutput';
|
||||
import IMyDataServiceInput from "./IMyDataServiceInput";
|
||||
|
@ -9,8 +9,6 @@ import IMyDataServiceInput from "./IMyDataServiceInput";
|
|||
export default class MyDataService {
|
||||
|
||||
private input: IMyDataServiceInput = null;
|
||||
private graphClient: MSGraphClient = this.input.mSGraphClient;
|
||||
private httpClient: HttpClient = this.input.httpClient;
|
||||
|
||||
constructor(input: IMyDataServiceInput) {
|
||||
this.input = input;
|
||||
|
@ -18,7 +16,7 @@ export default class MyDataService {
|
|||
|
||||
public async getAppDataFolder(): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
this.graphClient
|
||||
this.input.mSGraphClient
|
||||
.api(`/me/drive/special/approot/children?$filter=name eq '${this.input.appDataFolderName}'`)
|
||||
.version('v1.0')
|
||||
.get((error, response: any, rawResponse?: any) => {
|
||||
|
@ -51,7 +49,7 @@ export default class MyDataService {
|
|||
};
|
||||
|
||||
return new Promise<string>((resolve, reject) =>
|
||||
this.graphClient
|
||||
this.input.mSGraphClient
|
||||
.api('/me/drive/special/approot/children')
|
||||
.version('v1.0')
|
||||
.post(driveItem)
|
||||
|
@ -68,7 +66,7 @@ export default class MyDataService {
|
|||
const id = response.value.filter(item => item.name === this.input.appDataFolderName)[0].id;
|
||||
const stream = JSON.stringify(appData);
|
||||
|
||||
this.graphClient
|
||||
this.input.mSGraphClient
|
||||
.api(`/me/drive/items/${id}:/${this.input.appDataJsonFileName}:/content`)
|
||||
.version('v1.0')
|
||||
.put(stream);
|
||||
|
@ -77,7 +75,7 @@ export default class MyDataService {
|
|||
|
||||
public async getJsonAppDataFile(): Promise<IAppData> {
|
||||
return new Promise<IAppData>((resolve, reject) =>
|
||||
this.graphClient
|
||||
this.input.mSGraphClient
|
||||
.api(`/me/drive/special/approot:/${this.input.appDataFolderName}:/children?$filter=name eq '${this.input.appDataJsonFileName}'`)
|
||||
.version('v1.0')
|
||||
.get((error, response: any, rawResponse?: any) => {
|
||||
|
@ -88,12 +86,12 @@ export default class MyDataService {
|
|||
if (response.value.length === 0) {
|
||||
resolve(
|
||||
{
|
||||
userFallowedSites: []
|
||||
userFollowedSites: []
|
||||
} as IAppData);
|
||||
}
|
||||
|
||||
const downloadUrl = response.value.filter(item => item.name === this.input.appDataJsonFileName)[0]['@microsoft.graph.downloadUrl'];
|
||||
this.httpClient
|
||||
this.input.httpClient
|
||||
.get(downloadUrl, HttpClient.configurations.v1)
|
||||
.then((innerResponse: HttpClientResponse): Promise<string> => {
|
||||
if (innerResponse.ok) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/References.scss";
|
||||
|
||||
// colors
|
||||
$themePrimary: "[theme: themePrimary, default: #0078d7]";
|
||||
$neutralPrimary: "[theme: neutralPrimary, default: #333]";
|
||||
$neutralLight: "[theme: neutralLight, default: #eaeaea]";
|
||||
$neutralSecondary: "[theme: neutralSecondary, default: #666666]";
|
||||
$white: "[theme: white, default: #fff]";
|
||||
$black: "[theme: black, default: #000000]";
|
||||
|
||||
// general styling
|
||||
$marginSize: 9.4px;
|
||||
$marginStep: 20px;
|
||||
|
||||
// tiles -> tilePadding + tileHeight = tileSize
|
||||
$tileSize: 130px;
|
||||
$tilePadding: 35px;
|
||||
$tileHeight: 95px;
|
||||
$additionalMarginWhenNoIcon: 42px;
|
|
@ -0,0 +1,6 @@
|
|||
export default class Constants {
|
||||
public static appDataFolderName: string = 'followedSitesData';
|
||||
public static appDataJsonFileName: string = 'followedSitesSavedData.json';
|
||||
public static urlFollowedSites: string = 'webUrl';
|
||||
public static nameFollowedSites: string = 'displayName';
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import ISiteItem from './ISiteItem';
|
||||
import IFollowedSite from "./IFollowedSite";
|
||||
|
||||
export default interface IAppData {
|
||||
userFallowedSites: ISiteItem[];
|
||||
userFollowedSites: IFollowedSite[];
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export default interface IFollowedSite {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export default interface ISiteItem {
|
||||
id: number;
|
||||
url: string;
|
||||
}
|
Loading…
Reference in New Issue