added drag and drop follow sites webpart

This commit is contained in:
Adam 2022-01-09 16:38:17 +01:00
parent 5ebaaa252e
commit d6b14a440b
39 changed files with 2639 additions and 421 deletions

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -6,7 +6,7 @@
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",

View File

@ -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

View File

@ -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",

View File

@ -2,17 +2,23 @@ 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,
DragAndDropFollowedSites,
{
context: this.context
}

View File

@ -1 +0,0 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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 });
}
}

View File

@ -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[];
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1 @@
export default interface IErrorPanelProps { }

View File

@ -0,0 +1 @@
export default interface IErrorPanelState { }

View File

@ -0,0 +1 @@
export default interface INoItemsProps { }

View File

@ -0,0 +1 @@
export default interface INoItemsState { }

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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>
);
});

View File

@ -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;
}

View File

@ -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>
));

View File

@ -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 🧐"
}
});

View File

@ -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' {

View File

@ -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) {

View File

@ -1,7 +1,7 @@
import {
MSGraphClient,
HttpClient,
HttpClientResponse } from '@microsoft/sp-http';
import {
HttpClient,
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) {

View File

@ -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;

View File

@ -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';
}

View File

@ -1,5 +1,5 @@
import ISiteItem from './ISiteItem';
import IFollowedSite from "./IFollowedSite";
export default interface IAppData {
userFallowedSites: ISiteItem[];
userFollowedSites: IFollowedSite[];
}

View File

@ -0,0 +1,4 @@
export default interface IFollowedSite {
name: string;
url: string;
}

View File

@ -1,4 +0,0 @@
export default interface ISiteItem {
id: number;
url: string;
}