Merge pull request #2197 from Adam-it/add-react-webpart-fav-list-in-grid
Add react webpart with drag and drop followed sites
|
@ -0,0 +1,33 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
release
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"plusBeta": false,
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.13.0",
|
||||
"libraryName": "react-followed-drag-and-drop-grid",
|
||||
"libraryId": "513ca7e1-f774-438a-9790-80a4a3d08c9a",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": true,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
# Followed Drag and Drop Grid
|
||||
|
||||
## Summary
|
||||
|
||||
![image](./assets/sorting.gif)
|
||||
|
||||
This web part is a good example (starting point) for a solution to implement alternative view for user followed sites (or any kind of links). The web part 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 web part 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)
|
||||
|
||||
|
||||
## Compatibility
|
||||
|
||||
![SPFx 1.13.0](https://img.shields.io/badge/SPFx-1.13.0-green.svg)
|
||||
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
|
||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
||||
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
|
||||
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
|
||||
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-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)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
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)
|
||||
--------|---------
|
||||
react-followed-drag-and-drop-grid | [Adam Wójcik](https://github.com/Adam-it)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|January 09, 2022|Initial release
|
||||
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- Ensure that you are at the solution folder
|
||||
- in the command-line run:
|
||||
- `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
|
||||
|
||||
Description of the extension that expands upon high-level summary above.
|
||||
|
||||
This extension illustrates the following concepts:
|
||||
|
||||
- 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
|
||||
|
||||
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
|
||||
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
|
||||
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
|
||||
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
|
||||
|
||||
|
||||
|
||||
## Help
|
||||
|
||||
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
|
||||
|
||||
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
|
||||
|
||||
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-followed-drag-and-drop-grid%22) to see if anybody else is having the same issues.
|
||||
|
||||
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-followed-drag-and-drop-grid) and see what the community is saying.
|
||||
|
||||
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-followed-drag-and-drop-grid&template=bug-report.yml&sample=react-followed-drag-and-drop-grid&authors=@Adam-it&title=react-followed-drag-and-drop-grid%20-%20).
|
||||
|
||||
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-followed-drag-and-drop-grid&template=question.yml&sample=react-followed-drag-and-drop-grid&authors=@Adam-it&title=react-followed-drag-and-drop-grid%20-%20).
|
||||
|
||||
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-followed-drag-and-drop-grid&template=suggestion.yml&sample=react-followed-drag-and-drop-grid&authors=@Adam-it&title=react-followed-drag-and-drop-grid%20-%20).
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
|
||||
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-followed-drag-and-drop-grid" />
|
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,79 @@
|
|||
[
|
||||
{
|
||||
"name": "pnp-sp-dev-spfx-web-parts-react-followed-drag-and-drop-grid",
|
||||
"source": "pnp",
|
||||
"title": "Followed Drag and Drop Grid",
|
||||
"shortDescription": "This web part is a good example (starting point) for a solution to implement alternative view for user followed sites (or any kind of links). The web part uses Microsoft Graph so it presents how to define needed web api permissions in package-solution and use MS Graph API endpoints.",
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-followed-drag-and-drop-grid",
|
||||
"longDescription": [
|
||||
"YOUR-SHORT-DESCRIPTION"
|
||||
],
|
||||
"creationDateTime": "2022-01-09",
|
||||
"updateDateTime": "2022-01-09",
|
||||
"products": [
|
||||
"SharePoint"
|
||||
],
|
||||
"metadata": [
|
||||
{
|
||||
"key": "CLIENT-SIDE-DEV",
|
||||
"value": "React"
|
||||
},
|
||||
{
|
||||
"key": "SPFX-VERSION",
|
||||
"value": "1.13.0"
|
||||
}
|
||||
],
|
||||
"thumbnails": [
|
||||
{
|
||||
"type": "image",
|
||||
"order": 100,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-followed-drag-and-drop-grid/assets/mainImage.png",
|
||||
"alt": "Web Part Preview"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 101,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-followed-drag-and-drop-grid/assets/appInTeams.png",
|
||||
"alt": "App in Teams"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 102,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-followed-drag-and-drop-grid/assets/nothingToFollow.png",
|
||||
"alt": "Nothisg to follow"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 103,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-followed-drag-and-drop-grid/assets/sorting.gif",
|
||||
"alt": "Sorting"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 104,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-followed-drag-and-drop-grid/assets/dataAsJson.png",
|
||||
"alt": "Data as JSON"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 105,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-followed-drag-and-drop-grid/assets/linkSavedInJsonFile.png",
|
||||
"alt": "Link saved to JSON file"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"gitHubAccount": "Adam-it",
|
||||
"pictureUrl": "https://github.com/Adam-it.png",
|
||||
"name": "Adam Wójcik"
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"name": "Build your first SharePoint client-side web part",
|
||||
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
|
||||
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
After Width: | Height: | Size: 509 KiB |
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"drag-and-drop-followed-sites-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/dragAndDropFollowedSites/DragAndDropFollowedSitesWebPart.js",
|
||||
"manifest": "./src/webparts/dragAndDropFollowedSites/DragAndDropFollowedSitesWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"DragAndDropFollowedSitesWebPartStrings": "lib/webparts/dragAndDropFollowedSites/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./release/assets/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-followed-drag-and-drop-grid",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-followed-drag-and-drop-grid-client-side-solution",
|
||||
"id": "513ca7e1-f774-438a-9790-80a4a3d08c9a",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"developer": {
|
||||
"name": "",
|
||||
"websiteUrl": "",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"mpnId": "Undefined-1.13.0"
|
||||
},
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Sites.ReadWrite.All"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-followed-drag-and-drop-grid.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://tenanttocheck.sharepoint.com/sites/webpartfollowedSites/_layouts/workbench.aspx"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
var getTasks = build.rig.getTasks;
|
||||
build.rig.getTasks = function () {
|
||||
var result = getTasks.call(build.rig);
|
||||
|
||||
result.set('serve', result.get('serve-deprecated'));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
build.initialize(require('gulp'));
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "react-followed-drag-and-drop-grid",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@microsoft/sp-build-web": "1.13.0",
|
||||
"@microsoft/sp-tslint-rules": "1.13.0",
|
||||
"@microsoft/sp-module-interfaces": "1.13.0",
|
||||
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
|
||||
"gulp": "~4.0.2",
|
||||
"ajv": "~5.2.2",
|
||||
"@types/webpack-env": "1.13.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "b63d60a8-f42c-40ef-ad08-c893ea055c5c",
|
||||
"alias": "DragAndDropFollowedSitesWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
|
||||
"supportsThemeVariants": true,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Drag and drop followed sites" },
|
||||
"description": { "default": "DragAndDropFollowedSites description" },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"properties": {}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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/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,
|
||||
{
|
||||
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: [
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export interface IDragAndDropFollowedSitesWebPartProps {}
|
|
@ -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,5 @@
|
|||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
|
||||
export interface IDragAndDropFollowedSitesProps {
|
||||
context: WebPartContext;
|
||||
}
|
|
@ -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>
|
||||
));
|
11
samples/react-followed-drag-and-drop-grid/src/webparts/dragAndDropFollowedSites/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"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 🧐"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
declare interface IDragAndDropFollowedSitesWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
Title: string;
|
||||
Loading: string;
|
||||
ErrorText: string;
|
||||
ErrorPanelRefresh: string;
|
||||
ErrorCouldNotGetData: string;
|
||||
NoItemsText: string;
|
||||
}
|
||||
|
||||
declare module 'DragAndDropFollowedSitesWebPartStrings' {
|
||||
const strings: IDragAndDropFollowedSitesWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { MSGraphClient } from '@microsoft/sp-http';
|
||||
|
||||
export default class FollowedSitesService {
|
||||
|
||||
private graphClient: MSGraphClient = null;
|
||||
|
||||
constructor(graphClient: MSGraphClient) {
|
||||
this.graphClient = graphClient;
|
||||
}
|
||||
|
||||
public async getMyFollowedSites(): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
this.graphClient
|
||||
.api('/me/followedSites?$top=1000')
|
||||
.version('v1.0')
|
||||
.get((error, response: any, rawResponse?: any) => {
|
||||
if (error) {
|
||||
resolve(error);
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { MSGraphClient, HttpClient } from '@microsoft/sp-http';
|
||||
|
||||
export default interface IMyDataServiceInput {
|
||||
mSGraphClient: MSGraphClient;
|
||||
httpClient: HttpClient;
|
||||
appDataFolderName: string;
|
||||
appDataJsonFileName: string;
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import {
|
||||
HttpClient,
|
||||
HttpClientResponse
|
||||
} from '@microsoft/sp-http';
|
||||
import IAppData from '../../../model/IAppData';
|
||||
import IAppDataFolderExistsOutput from '../../../model/IAppDataFolderExistsOutput';
|
||||
import IMyDataServiceInput from "./IMyDataServiceInput";
|
||||
|
||||
export default class MyDataService {
|
||||
|
||||
private input: IMyDataServiceInput = null;
|
||||
|
||||
constructor(input: IMyDataServiceInput) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public async getAppDataFolder(): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
this.input.mSGraphClient
|
||||
.api(`/me/drive/special/approot/children?$filter=name eq '${this.input.appDataFolderName}'`)
|
||||
.version('v1.0')
|
||||
.get((error, response: any, rawResponse?: any) => {
|
||||
if (error) {
|
||||
resolve(error);
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
}));
|
||||
}
|
||||
|
||||
public async checkIfAppDataFolderExists(): Promise<IAppDataFolderExistsOutput> {
|
||||
return new Promise<IAppDataFolderExistsOutput>((resolve, reject) => {
|
||||
resolve(this.getAppDataFolder().then(result => {
|
||||
const isError = result.errorCode !== undefined;
|
||||
return {
|
||||
isError,
|
||||
errorMessage: isError ? result.errorMessage : '',
|
||||
folderExists: !isError ? result.value.some(item => item.name === this.input.appDataFolderName) : false
|
||||
} as IAppDataFolderExistsOutput;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
public async createAppDataFolder(): Promise<string> {
|
||||
const driveItem = {
|
||||
name: this.input.appDataFolderName,
|
||||
folder: {},
|
||||
'@microsoft.graph.conflictBehavior': 'fail'
|
||||
};
|
||||
|
||||
return new Promise<string>((resolve, reject) =>
|
||||
this.input.mSGraphClient
|
||||
.api('/me/drive/special/approot/children')
|
||||
.version('v1.0')
|
||||
.post(driveItem)
|
||||
.then(result => {
|
||||
if (result != null) {
|
||||
resolve(result.name.toString());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public createOrUpdateJsonDataFile(appData: IAppData): void {
|
||||
this.getAppDataFolder()
|
||||
.then(response => {
|
||||
const id = response.value.filter(item => item.name === this.input.appDataFolderName)[0].id;
|
||||
const stream = JSON.stringify(appData);
|
||||
|
||||
this.input.mSGraphClient
|
||||
.api(`/me/drive/items/${id}:/${this.input.appDataJsonFileName}:/content`)
|
||||
.version('v1.0')
|
||||
.put(stream);
|
||||
});
|
||||
}
|
||||
|
||||
public async getJsonAppDataFile(): Promise<IAppData> {
|
||||
return new Promise<IAppData>((resolve, reject) =>
|
||||
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) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.value.length === 0) {
|
||||
resolve(
|
||||
{
|
||||
userFollowedSites: []
|
||||
} as IAppData);
|
||||
}
|
||||
|
||||
const downloadUrl = response.value.filter(item => item.name === this.input.appDataJsonFileName)[0]['@microsoft.graph.downloadUrl'];
|
||||
this.input.httpClient
|
||||
.get(downloadUrl, HttpClient.configurations.v1)
|
||||
.then((innerResponse: HttpClientResponse): Promise<string> => {
|
||||
if (innerResponse.ok) {
|
||||
return innerResponse.text();
|
||||
}
|
||||
|
||||
return Promise.reject(innerResponse.statusText);
|
||||
})
|
||||
.then((settingsString: string) => {
|
||||
const settings: IAppData = JSON.parse(settingsString);
|
||||
resolve(settings);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import IFollowedSite from "./IFollowedSite";
|
||||
|
||||
export default interface IAppData {
|
||||
userFollowedSites: IFollowedSite[];
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export default interface IAppDataFolderExistsOutput {
|
||||
isError: boolean;
|
||||
errorMessage: string;
|
||||
folderExists: boolean;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export default interface IFollowedSite {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 542 B |
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.9/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection",
|
||||
"es2015.promise"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|