├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .yo-rc.json ├── LICENSE ├── README.md ├── config ├── config.json ├── copy-assets.json ├── deploy-azure-storage.json ├── package-solution.json ├── serve.json └── write-manifests.json ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── controls │ └── PropertyPaneColorPalette │ │ ├── PropertyPaneColorPalette.ts │ │ └── components │ │ ├── ColorPalette.module.scss │ │ ├── ColorPalette.tsx │ │ ├── ColorSwatch.module.scss │ │ └── ColorSwatch.tsx ├── index.ts ├── services │ └── SharePoint │ │ ├── IList.ts │ │ ├── IListField.ts │ │ ├── IListItem.ts │ │ ├── SharePointService.ts │ │ └── data │ │ ├── MockListCollection.ts │ │ ├── MockListFieldCollection.ts │ │ └── MockListItemCollection.ts └── webparts │ └── dash │ ├── DashWebPart.manifest.json │ ├── DashWebPart.ts │ ├── components │ ├── Chart.module.scss │ ├── Chart.tsx │ ├── Dash.tsx │ └── IDashProps.ts │ └── loc │ ├── en-us.js │ └── mystrings.d.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [{package,bower}.json] 24 | indent_style = space 25 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # Build generated files 10 | dist 11 | lib 12 | solution 13 | temp 14 | *.sppkg 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # OSX 20 | .DS_Store 21 | 22 | # Visual Studio files 23 | .ntvs_analysis.dat 24 | .vs 25 | bin 26 | obj 27 | 28 | # Resx Generated Code 29 | *.resx.ts 30 | 31 | # Styles Generated Code 32 | *.scss.ts 33 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "msjsdiag.debugger-for-chrome" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * Install Chrome Debugger Extension for Visual Studio Code to debug your components with the 4 | * Chrome browser: https://aka.ms/spfx-debugger-extensions 5 | */ 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Local workbench", 9 | "type": "chrome", 10 | "request": "launch", 11 | "url": "https://localhost:4321/temp/workbench.html", 12 | "webRoot": "${workspaceRoot}", 13 | "sourceMaps": true, 14 | "sourceMapPathOverrides": { 15 | "webpack:///../../../src/*": "${webRoot}/src/*", 16 | "webpack:///../../../../src/*": "${webRoot}/src/*", 17 | "webpack:///../../../../../src/*": "${webRoot}/src/*" 18 | }, 19 | "runtimeArgs": [ 20 | "--remote-debugging-port=9222" 21 | ] 22 | }, 23 | { 24 | "name": "Hosted workbench", 25 | "type": "chrome", 26 | "request": "launch", 27 | "url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx", 28 | "webRoot": "${workspaceRoot}", 29 | "sourceMaps": true, 30 | "sourceMapPathOverrides": { 31 | "webpack:///../../../src/*": "${webRoot}/src/*", 32 | "webpack:///../../../../src/*": "${webRoot}/src/*", 33 | "webpack:///../../../../../src/*": "${webRoot}/src/*" 34 | }, 35 | "runtimeArgs": [ 36 | "--remote-debugging-port=9222", 37 | "-incognito" 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Configure glob patterns for excluding files and folders in the file explorer. 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.DS_Store": true, 7 | "**/bower_components": true, 8 | "**/coverage": true, 9 | "**/lib-amd": true, 10 | "src/**/*.scss.ts": true 11 | }, 12 | "typescript.tsdk": ".\\node_modules\\typescript\\lib" 13 | } -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@microsoft/generator-sharepoint": { 3 | "isCreatingSolution": true, 4 | "environment": "spo", 5 | "version": "1.6.0", 6 | "libraryName": "dash", 7 | "libraryId": "0ff6d6a6-c11f-4056-9c17-fe87bf4cc89b", 8 | "packageManager": "npm", 9 | "componentType": "webpart" 10 | } 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Spiritous, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## dash 2 | 3 | This is a SharePoint list data visualization web part built in SharePoint Framework (SPFx) using the React SPFx template. This web part utilizes [React](https://reactjs.org/), [SharePoint REST web services](https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/get-to-know-the-sharepoint-rest-service), and [Chart.js](http://www.chartjs.org/). 4 | 5 | ### Building Your Own Web Part 6 | 7 | This solution is intended to accompany [Introduction to SharePoint Framework](https://sharepointfx.io/), an online educational course that helps you to learn modern SharePoint Framework development techniques. Learn how to build your own dashboard web part by following the lessons found at [sharepointfx.io](https://sharepointfx.io/). 8 | 9 | ### Getting Started 10 | 11 | ```bash 12 | # Install dependencies 13 | npm i 14 | 15 | # Run the local workbench 16 | gulp serve 17 | ``` 18 | 19 | ### Deploying to SharePoint 20 | 21 | ```bash 22 | # Bundle the solution 23 | gulp bundle --ship 24 | 25 | # Package the solution 26 | # - This creates a sharepoint/solution/dash.sppkg file 27 | gulp package-solution --ship 28 | ``` 29 | 30 | Once you have a `dash.sppkg` file, you can deploy this to your SharePoint environment's [App Catalog](https://docs.microsoft.com/en-us/sharepoint/use-app-catalog). See the **Deploying and Updating Solutions** lesson for more information on solution deployment. 31 | 32 | ### Learn More 33 | For more information about the structure and functionality of this solution, see the [official SharePoint Framework documentation](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview). 34 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", 3 | "version": "2.0", 4 | "bundles": { 5 | "dash-web-part": { 6 | "components": [ 7 | { 8 | "entrypoint": "./lib/webparts/dash/DashWebPart.js", 9 | "manifest": "./src/webparts/dash/DashWebPart.manifest.json" 10 | } 11 | ] 12 | } 13 | }, 14 | "externals": {}, 15 | "localizedResources": { 16 | "DashWebPartStrings": "lib/webparts/dash/loc/{locale}.js", 17 | "PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js" 18 | } 19 | } -------------------------------------------------------------------------------- /config/copy-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json", 3 | "deployCdnPath": "temp/deploy" 4 | } 5 | -------------------------------------------------------------------------------- /config/deploy-azure-storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", 3 | "workingDir": "./temp/deploy/", 4 | "account": "", 5 | "container": "dash", 6 | "accessKey": "" 7 | } -------------------------------------------------------------------------------- /config/package-solution.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", 3 | "solution": { 4 | "name": "dash-client-side-solution", 5 | "id": "0ff6d6a6-c11f-4056-9c17-fe87bf4cc89b", 6 | "version": "1.1.0.0", 7 | "includeClientSideAssets": true, 8 | "skipFeatureDeployment": true 9 | }, 10 | "paths": { 11 | "zippedPackage": "solution/dash.sppkg" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json", 3 | "port": 4321, 4 | "https": true, 5 | "initialPage": "https://localhost:5432/workbench", 6 | "api": { 7 | "port": 5432, 8 | "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/write-manifests.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", 3 | "cdnBasePath": "" 4 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const build = require('@microsoft/sp-build-web'); 5 | build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); 6 | 7 | build.initialize(gulp); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dash", 3 | "version": "0.0.1", 4 | "private": true, 5 | "engines": { 6 | "node": ">=0.10.0" 7 | }, 8 | "scripts": { 9 | "build": "gulp bundle", 10 | "clean": "gulp clean", 11 | "test": "gulp test" 12 | }, 13 | "dependencies": { 14 | "@microsoft/sp-core-library": "1.6.0", 15 | "@microsoft/sp-lodash-subset": "1.6.0", 16 | "@microsoft/sp-office-ui-fabric-core": "1.6.0", 17 | "@microsoft/sp-webpart-base": "1.6.0", 18 | "@pnp/spfx-property-controls": "1.11.0", 19 | "@types/es6-promise": "0.0.33", 20 | "@types/react": "15.6.6", 21 | "@types/react-dom": "15.5.6", 22 | "@types/webpack-env": "1.13.1", 23 | "chart.js": "^2.7.3", 24 | "react": "15.6.2", 25 | "react-chartjs-2": "^2.7.4", 26 | "react-dom": "15.6.2" 27 | }, 28 | "devDependencies": { 29 | "@microsoft/sp-build-web": "1.6.0", 30 | "@microsoft/sp-module-interfaces": "1.6.0", 31 | "@microsoft/sp-webpart-workbench": "1.6.0", 32 | "tslint-microsoft-contrib": "~5.0.0", 33 | "gulp": "~3.9.1", 34 | "@types/chai": "3.4.34", 35 | "@types/mocha": "2.2.38", 36 | "ajv": "~5.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/controls/PropertyPaneColorPalette/PropertyPaneColorPalette.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDom from 'react-dom'; 3 | import { 4 | IPropertyPaneField, 5 | PropertyPaneFieldType, 6 | IPropertyPaneCustomFieldProps, 7 | } from '@microsoft/sp-webpart-base'; 8 | import { ColorPalette, IColorPaletteProps } from './components/ColorPalette'; 9 | 10 | export interface IPropertyPaneColorPaletteProps { 11 | label: string; 12 | key: string; 13 | colors: string[]; 14 | onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void; 15 | disabled?: boolean; 16 | } 17 | 18 | export interface IPropertyPaneColorPaletteInternalProps extends IPropertyPaneColorPaletteProps, IPropertyPaneCustomFieldProps {} 19 | 20 | export class PropertyPaneColorPalette implements IPropertyPaneField { 21 | public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom; 22 | public targetProperty: string; 23 | public properties: IPropertyPaneColorPaletteInternalProps; 24 | private elem: HTMLElement; 25 | 26 | constructor(targetProperty: string, properties: IPropertyPaneColorPaletteProps) { 27 | this.targetProperty = targetProperty; 28 | this.properties = { 29 | label: properties.label, 30 | key: properties.key, 31 | colors: properties.colors, 32 | onPropertyChange: properties.onPropertyChange, 33 | disabled: properties.disabled, 34 | onRender: this.onRender.bind(this), 35 | }; 36 | } 37 | 38 | public render(): void { 39 | if (!this.elem) return; 40 | this.onRender(this.elem); 41 | } 42 | 43 | private onRender(elem: HTMLElement): void { 44 | if (!this.elem) this.elem = elem; 45 | 46 | const element: React.ReactElement<{}> = React.createElement(ColorPalette, { 47 | colors: this.properties.colors, 48 | disabled: this.properties.disabled, 49 | onChanged: this.onChanged.bind(this), 50 | key: this.properties.key, 51 | }); 52 | 53 | ReactDom.render(element, elem); 54 | } 55 | 56 | private onChanged(colors: string[]): void { 57 | this.properties.onPropertyChange(this.targetProperty, this.properties.colors, colors); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/controls/PropertyPaneColorPalette/components/ColorPalette.module.scss: -------------------------------------------------------------------------------- 1 | .colorPalette { 2 | color: black; 3 | } 4 | 5 | .colorGrid { 6 | > * { 7 | float: left; 8 | margin: 0 5px 5px 0; 9 | } 10 | } 11 | 12 | .addColorBtn { 13 | background: #eee; 14 | border: 0; 15 | color: #ccc; 16 | cursor: pointer; 17 | height: 40px; 18 | width: 40px; 19 | padding: 0; 20 | text-align: center; 21 | font-size: 20px; 22 | line-height: 40px; 23 | 24 | &:hover, 25 | &:focus { 26 | background-color: darken(#eee, 3%); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/controls/PropertyPaneColorPalette/components/ColorPalette.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ColorSwatch from './ColorSwatch'; 3 | import styles from './ColorPalette.module.scss'; 4 | import * as strings from 'DashWebPartStrings'; 5 | 6 | export interface IColorPaletteProps { 7 | colors: string[]; 8 | disabled?: boolean; 9 | onChanged(colors: string[]): void; 10 | } 11 | 12 | export class ColorPalette extends React.Component { 13 | constructor(props: IColorPaletteProps) { 14 | super(props); 15 | 16 | // Bind methods 17 | this.onChanged = this.onChanged.bind(this); 18 | this.addColor = this.addColor.bind(this); 19 | } 20 | 21 | public render(): JSX.Element { 22 | return ( 23 |
24 | {this.props.colors.map((color, i) => { 25 | return ( 26 | this.onChanged(newColor, i)} onColorDeleted={() => this.onChanged(null, i)} /> 27 | ); 28 | })} 29 | 33 |
34 | ); 35 | } 36 | 37 | public onChanged(newColor: string, index: number): void { 38 | const updatedColors = this.props.colors; 39 | updatedColors[index] = newColor; 40 | 41 | if (newColor === null) { 42 | updatedColors.splice(index, 1); 43 | } 44 | 45 | this.props.onChanged(updatedColors); 46 | } 47 | 48 | public addColor(): void { 49 | const updatedColors = this.props.colors; 50 | updatedColors.push('#000000'); 51 | 52 | this.props.onChanged(updatedColors); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/controls/PropertyPaneColorPalette/components/ColorSwatch.module.scss: -------------------------------------------------------------------------------- 1 | .colorSwatch { 2 | border: 1px solid #ddd; 3 | cursor: pointer; 4 | height: 40px; 5 | width: 40px; 6 | padding: 0; 7 | text-indent: -9999em; 8 | } 9 | 10 | .swatchActions { 11 | padding: 0 18px 18px; 12 | text-align: right; 13 | } 14 | -------------------------------------------------------------------------------- /src/controls/PropertyPaneColorPalette/components/ColorSwatch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColorPicker } from 'office-ui-fabric-react/lib/ColorPicker'; 3 | import { Callout, DirectionalHint } from 'office-ui-fabric-react/lib/Callout'; 4 | import { createRef } from 'office-ui-fabric-react/lib/Utilities'; 5 | import { DefaultButton, IButtonProps } from 'office-ui-fabric-react/lib/Button'; 6 | import styles from './ColorSwatch.module.scss'; 7 | import * as strings from 'DashWebPartStrings'; 8 | 9 | export interface IColorSwatchProps { 10 | color: string; 11 | onColorChanged(color: string): void; 12 | onColorDeleted(): void; 13 | } 14 | 15 | export interface IColorSwatchState { 16 | picking: boolean; 17 | } 18 | 19 | export default class ColorSwatch extends React.Component { 20 | private pickBtn = createRef(); 21 | 22 | constructor(props: IColorSwatchProps) { 23 | super(props); 24 | 25 | // Bind methods 26 | this.pick = this.pick.bind(this); 27 | 28 | // Default state 29 | this.state = { 30 | picking: false, 31 | }; 32 | } 33 | 34 | public render(): JSX.Element { 35 | return ( 36 |
37 | 38 | 39 | 45 |
46 | ); 47 | } 48 | 49 | public pick(): void { 50 | this.setState({ 51 | picking: !this.state.picking, 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // A file is required to be in the root of the /src directory by the TypeScript compiler 2 | -------------------------------------------------------------------------------- /src/services/SharePoint/IList.ts: -------------------------------------------------------------------------------- 1 | export interface IList { 2 | Id: string; 3 | Title: string; 4 | [index: string]: any; 5 | } 6 | 7 | export interface IListCollection { 8 | value: IList[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/services/SharePoint/IListField.ts: -------------------------------------------------------------------------------- 1 | export interface IListField { 2 | Id: string; 3 | Title: string; 4 | InternalName: string; 5 | TypeAsString: string; 6 | [index: string]: any; 7 | } 8 | 9 | export interface IListFieldCollection { 10 | value: IListField[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/services/SharePoint/IListItem.ts: -------------------------------------------------------------------------------- 1 | export interface IListItem { 2 | Id: number; 3 | [index: string]: any; 4 | } 5 | 6 | export interface IListItemCollection { 7 | value: IListItem[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/services/SharePoint/SharePointService.ts: -------------------------------------------------------------------------------- 1 | import { WebPartContext } from "@microsoft/sp-webpart-base"; 2 | import { EnvironmentType } from "@microsoft/sp-core-library"; 3 | import { SPHttpClient } from "@microsoft/sp-http"; 4 | import { IListCollection } from "./IList"; 5 | import { IListItemCollection } from "./IListItem"; 6 | import { IListFieldCollection } from "./IListField"; 7 | import { MockListCollection } from './data/MockListCollection'; 8 | import { MockListItemCollection } from './data/MockListItemCollection'; 9 | import { MockListFieldCollection } from './data/MockListFieldCollection'; 10 | 11 | export class SharePointServiceManager { 12 | public context: WebPartContext; 13 | public environmentType: EnvironmentType; 14 | 15 | public setup(context: WebPartContext, environmentType: EnvironmentType): void { 16 | this.context = context; 17 | this.environmentType = environmentType; 18 | } 19 | 20 | public get(relativeEndpointUrl: string): Promise { 21 | return this.context.spHttpClient.get(`${this.context.pageContext.web.absoluteUrl}${relativeEndpointUrl}`, SPHttpClient.configurations.v1).then(response => { 22 | if (!response.ok) return Promise.reject('GET Request Failed'); 23 | return response.json(); 24 | }).catch(error => { 25 | return Promise.reject(error); 26 | }); 27 | } 28 | 29 | public getLists(showHiddenLists: boolean = false): Promise { 30 | if (this.environmentType == EnvironmentType.Local) { 31 | return new Promise(resolve => resolve(MockListCollection)); 32 | } 33 | return this.get(`/_api/lists${!showHiddenLists ? '?$filter=Hidden eq false' : ''}`); 34 | } 35 | 36 | public getListItems(listId: string, selectedFields?: string[]): Promise { 37 | if (this.environmentType == EnvironmentType.Local) { 38 | return new Promise(resolve => resolve(MockListItemCollection)); 39 | } 40 | return this.get(`/_api/lists/getbyid('${listId}')/items${selectedFields ? `?$select=${selectedFields.join(',')}` : ''}`); 41 | } 42 | 43 | public getListFields(listId: string, showHiddenFields: boolean = false): Promise { 44 | if (this.environmentType == EnvironmentType.Local) { 45 | return new Promise(resolve => resolve(MockListFieldCollection)); 46 | } 47 | return this.get(`/_api/lists/getbyid('${listId}')/fields${!showHiddenFields ? '?$filter=Hidden eq false' : ''}`); 48 | } 49 | } 50 | 51 | const SharePointService = new SharePointServiceManager(); 52 | export default SharePointService; 53 | -------------------------------------------------------------------------------- /src/services/SharePoint/data/MockListCollection.ts: -------------------------------------------------------------------------------- 1 | import { IListCollection } from "../IList"; 2 | 3 | export const MockListCollection: IListCollection = { 4 | value: [ 5 | { 6 | Id: '77a1358e-50fc-414e-b72c-8cc9e8f717ba', 7 | Title: 'Sample List One', 8 | }, 9 | { 10 | Id: 'eeb559b7-5e3e-479c-a4cd-f48b046b54e7', 11 | Title: 'Sample List Two', 12 | }, 13 | { 14 | Id: '5b627813-79a4-456e-a341-324a065c919e', 15 | Title: 'Sample List Three', 16 | }, 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /src/services/SharePoint/data/MockListFieldCollection.ts: -------------------------------------------------------------------------------- 1 | import { IListFieldCollection } from "../IListField"; 2 | 3 | export const MockListFieldCollection: IListFieldCollection = { 4 | value: [ 5 | { 6 | Id: '9582db18-ce75-4087-8829-7bb99082d03f', 7 | InternalName: 'Title', 8 | Title: 'Title', 9 | TypeAsString: 'Text', 10 | }, 11 | { 12 | Id: 'c7b9073e-b0b0-4348-bb46-fd13c970e4c9', 13 | InternalName: 'EarningsQ1', 14 | Title: 'EarningsQ1', 15 | TypeAsString: 'Currency', 16 | }, 17 | { 18 | Id: '295da1c3-b0c9-43e4-bcbd-923ea5c705a4', 19 | InternalName: 'EarningsQ2', 20 | Title: 'EarningsQ2', 21 | TypeAsString: 'Currency', 22 | }, 23 | { 24 | Id: '36313fdc-540e-4494-a067-99082213f02d', 25 | InternalName: 'EarningsQ3', 26 | Title: 'EarningsQ3', 27 | TypeAsString: 'Currency', 28 | }, 29 | { 30 | Id: '857cc172-e01a-4b70-bb3f-b2997517e31e', 31 | InternalName: 'EarningsQ4', 32 | Title: 'EarningsQ4', 33 | TypeAsString: 'Currency', 34 | }, 35 | { 36 | Id: '5e039896-efc6-42c3-8c81-583dd013a4e9', 37 | InternalName: 'ID', 38 | Title: 'ID', 39 | TypeAsString: 'Counter', 40 | }, 41 | ], 42 | }; 43 | -------------------------------------------------------------------------------- /src/services/SharePoint/data/MockListItemCollection.ts: -------------------------------------------------------------------------------- 1 | import { IListItemCollection } from "../IListItem"; 2 | 3 | export const MockListItemCollection: IListItemCollection = { 4 | value: [ 5 | { 6 | Id: 0, 7 | Title: '2013', 8 | EarningsQ1: 854, 9 | EarningsQ2: 251, 10 | EarningsQ3: 703, 11 | EarningsQ4: 464, 12 | Total: 2272, 13 | }, 14 | { 15 | Id: 1, 16 | Title: '2014', 17 | EarningsQ1: 915, 18 | EarningsQ2: 194, 19 | EarningsQ3: 848, 20 | EarningsQ4: 990, 21 | Total: 2947, 22 | }, 23 | { 24 | Id: 2, 25 | Title: '2015', 26 | EarningsQ1: 830, 27 | EarningsQ2: 893, 28 | EarningsQ3: 28, 29 | EarningsQ4: 574, 30 | Total: 2325, 31 | }, 32 | { 33 | Id: 3, 34 | Title: '2016', 35 | EarningsQ1: 582, 36 | EarningsQ2: 813, 37 | EarningsQ3: 812, 38 | EarningsQ4: 967, 39 | Total: 3174, 40 | }, 41 | { 42 | Id: 4, 43 | Title: '2017', 44 | EarningsQ1: 181, 45 | EarningsQ2: 166, 46 | EarningsQ3: 51, 47 | EarningsQ4: 475, 48 | Total: 873, 49 | }, 50 | { 51 | Id: 5, 52 | Title: '2018', 53 | EarningsQ1: 802, 54 | EarningsQ2: 760, 55 | EarningsQ3: 916, 56 | EarningsQ4: 401, 57 | Total: 2879, 58 | }, 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /src/webparts/dash/DashWebPart.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", 3 | "id": "d8669698-3f70-40ac-8667-61c02ba914a8", 4 | "alias": "DashWebPart", 5 | "componentType": "WebPart", 6 | 7 | // The "*" signifies that the version should be taken from the package.json 8 | "version": "*", 9 | "manifestVersion": 2, 10 | 11 | // If true, the component can only be installed on sites where Custom Script is allowed. 12 | // Components that allow authors to embed arbitrary script code should set this to true. 13 | // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f 14 | "requiresCustomScript": false, 15 | 16 | "preconfiguredEntries": [{ 17 | "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other 18 | "group": { "default": "Other" }, 19 | "title": { "default": "Dash" }, 20 | "description": { "default": "Dash description" }, 21 | "officeFabricIconFontName": "BarChart4", 22 | "properties": { 23 | "listId": "", 24 | "selectedFields": [], 25 | "chartType": "Bar", 26 | "chartTitle": "", 27 | "colors": [ 28 | "#0078d4", 29 | "#bad80a", 30 | "#00b294" 31 | ] 32 | } 33 | }] 34 | } 35 | -------------------------------------------------------------------------------- /src/webparts/dash/DashWebPart.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDom from 'react-dom'; 3 | import { Version, Environment } from '@microsoft/sp-core-library'; 4 | import { 5 | BaseClientSideWebPart, 6 | IPropertyPaneConfiguration, 7 | PropertyPaneTextField, 8 | PropertyPaneDropdown, 9 | IPropertyPaneDropdownOption 10 | } from '@microsoft/sp-webpart-base'; 11 | import { 12 | PropertyFieldColorPicker, 13 | PropertyFieldColorPickerStyle, 14 | } from '@pnp/spfx-property-controls/lib/PropertyFieldColorPicker'; 15 | import { PropertyFieldMultiSelect } from '@pnp/spfx-property-controls/lib/PropertyFieldMultiSelect'; 16 | 17 | import { PropertyPaneColorPalette } from '../../controls/PropertyPaneColorPalette/PropertyPaneColorPalette'; 18 | import * as strings from 'DashWebPartStrings'; 19 | import Dash from './components/Dash'; 20 | import { IDashProps } from './components/IDashProps'; 21 | import SharePointService from '../../services/SharePoint/SharePointService'; 22 | import { updateA } from 'office-ui-fabric-react/lib/utilities/color'; 23 | 24 | export interface IDashWebPartProps { 25 | listId: string; 26 | selectedFields: string[]; 27 | chartType: string; 28 | chartTitle: string; 29 | colors: string[]; 30 | } 31 | 32 | export default class DashWebPart extends BaseClientSideWebPart { 33 | // List options state 34 | private listOptions: IPropertyPaneDropdownOption[]; 35 | private listOptionsLoading: boolean = false; 36 | 37 | // Field options state 38 | private fieldOptions: IPropertyPaneDropdownOption[]; 39 | private fieldOptionsLoading: boolean = false; 40 | 41 | public render(): void { 42 | const element: React.ReactElement = React.createElement( 43 | Dash, 44 | { 45 | listId: this.properties.listId, 46 | selectedFields: this.properties.selectedFields, 47 | chartType: this.properties.chartType, 48 | chartTitle: this.properties.chartTitle, 49 | colors: this.properties.colors, 50 | } 51 | ); 52 | 53 | ReactDom.render(element, this.domElement); 54 | } 55 | 56 | public onInit(): Promise { 57 | return super.onInit().then(() => { 58 | SharePointService.setup(this.context, Environment.type); 59 | }); 60 | } 61 | 62 | protected onDispose(): void { 63 | ReactDom.unmountComponentAtNode(this.domElement); 64 | } 65 | 66 | protected get dataVersion(): Version { 67 | return Version.parse('1.0'); 68 | } 69 | 70 | protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { 71 | return { 72 | pages: [ 73 | { 74 | header: { 75 | description: strings.PropertyPaneDescription 76 | }, 77 | groups: [ 78 | { 79 | groupName: strings.ChartData, 80 | groupFields: [ 81 | PropertyPaneDropdown('listId', { 82 | label: strings.List, 83 | options: this.listOptions, 84 | disabled: this.listOptionsLoading, 85 | }), 86 | PropertyFieldMultiSelect('selectedFields', { 87 | key: 'selectedFields', 88 | label: strings.SelectedFields, 89 | options: this.fieldOptions, 90 | disabled: this.fieldOptionsLoading, 91 | selectedKeys: this.properties.selectedFields, 92 | }) 93 | ] 94 | }, 95 | { 96 | groupName: strings.ChartSettings, 97 | groupFields: [ 98 | PropertyPaneDropdown('chartType', { 99 | label: strings.ChartType, 100 | options: [ 101 | { key: 'Bar', text: strings.ChartBar }, 102 | { key: 'HorizontalBar', text: strings.ChartBarHorizontal }, 103 | { key: 'Line', text: strings.ChartLine }, 104 | { key: 'Pie', text: strings.ChartPie }, 105 | { key: 'Doughnut', text: strings.ChartDonut }, 106 | ], 107 | }), 108 | PropertyPaneTextField('chartTitle', { 109 | label: strings.ChartTitle 110 | }), 111 | ], 112 | }, 113 | { 114 | groupName: strings.ChartStyle, 115 | groupFields: [ 116 | new PropertyPaneColorPalette('colors', { 117 | label: strings.Colors, 118 | colors: this.properties.colors, 119 | onPropertyChange: this.onPropertyPaneFieldChanged.bind(this), 120 | key: 'colors_palette', 121 | }), 122 | ], 123 | } 124 | ] 125 | } 126 | ] 127 | }; 128 | } 129 | 130 | private getLists(): Promise { 131 | this.listOptionsLoading = true; 132 | this.context.propertyPane.refresh(); 133 | 134 | return SharePointService.getLists().then(lists => { 135 | this.listOptionsLoading = false; 136 | this.context.propertyPane.refresh(); 137 | 138 | return lists.value.map(list => { 139 | return { 140 | key: list.Id, 141 | text: list.Title, 142 | }; 143 | }); 144 | }); 145 | } 146 | 147 | public getFields(): Promise { 148 | // No list selected 149 | if (!this.properties.listId) return Promise.resolve(); 150 | 151 | this.fieldOptionsLoading = true; 152 | this.context.propertyPane.refresh(); 153 | 154 | return SharePointService.getListFields(this.properties.listId).then(fields => { 155 | this.fieldOptionsLoading = false; 156 | this.context.propertyPane.refresh(); 157 | 158 | return fields.value.map(field => { 159 | return { 160 | key: field.InternalName, 161 | text: `${field.Title} (${field.TypeAsString})`, 162 | }; 163 | }); 164 | }); 165 | } 166 | 167 | protected onPropertyPaneConfigurationStart(): void { 168 | this.getLists().then(listOptions => { 169 | this.listOptions = listOptions; 170 | this.context.propertyPane.refresh(); 171 | }).then(() => { 172 | this.getFields().then(fieldOptions => { 173 | this.fieldOptions = fieldOptions; 174 | this.context.propertyPane.refresh(); 175 | }); 176 | }); 177 | } 178 | 179 | protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { 180 | super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue); 181 | 182 | if (propertyPath === 'listId' && newValue) { 183 | this.properties.selectedFields = []; 184 | 185 | this.getFields().then(fieldOptions => { 186 | this.fieldOptions = fieldOptions; 187 | this.context.propertyPane.refresh(); 188 | }); 189 | } 190 | 191 | else if (propertyPath === 'colors' && newValue) { 192 | this.properties.colors = newValue; 193 | this.context.propertyPane.refresh(); 194 | this.render(); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/webparts/dash/components/Chart.module.scss: -------------------------------------------------------------------------------- 1 | .chartTitle { 2 | text-align: center; 3 | font-weight: 200; 4 | font-size: 2.5em; 5 | margin: 0 0 10px; 6 | } 7 | 8 | .chartBody { 9 | position: relative; 10 | 11 | .chartSpinner { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | bottom: 0; 17 | background: rgba(255, 255, 255, 0.7); 18 | padding: 30px 0; 19 | z-index: 1; 20 | } 21 | } 22 | 23 | .chartFooter { 24 | text-align: center; 25 | } 26 | -------------------------------------------------------------------------------- /src/webparts/dash/components/Chart.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IListItem } from '../../../services/SharePoint/IListItem'; 3 | import SharePointService from '../../../services/SharePoint/SharePointService'; 4 | import { 5 | Bar, 6 | Line, 7 | HorizontalBar, 8 | Pie, 9 | Doughnut, 10 | } from 'react-chartjs-2'; 11 | import styles from './Chart.module.scss'; 12 | import { ActionButton } from 'office-ui-fabric-react/lib/Button'; 13 | import { 14 | Spinner, 15 | SpinnerSize, 16 | } from 'office-ui-fabric-react/lib/Spinner'; 17 | import { 18 | MessageBar, 19 | MessageBarType, 20 | } from 'office-ui-fabric-react/lib/MessageBar'; 21 | import * as strings from 'DashWebPartStrings'; 22 | 23 | export interface IChartProps { 24 | listId: string; 25 | selectedFields: string[]; 26 | chartType: string; 27 | chartTitle: string; 28 | colors: string[]; 29 | } 30 | 31 | export interface IChartState { 32 | items: IListItem[]; 33 | loading: boolean; 34 | error: string | null; 35 | } 36 | 37 | export default class Chart extends React.Component { 38 | constructor(props: IChartProps) { 39 | super(props); 40 | 41 | // Bind methods 42 | this.getItems = this.getItems.bind(this); 43 | this.chartData = this.chartData.bind(this); 44 | 45 | // Set initial state 46 | this.state = { 47 | items: [], 48 | loading: false, 49 | error: null, 50 | }; 51 | } 52 | 53 | public render(): JSX.Element { 54 | return ( 55 |
56 |

{this.props.chartTitle}

57 | 58 | {this.state.error && {this.state.error}} 59 | 60 |
61 | {this.state.loading && } 62 | 63 | {this.props.chartType == 'Bar' && } 64 | {this.props.chartType == 'Line' && } 65 | {this.props.chartType == 'HorizontalBar' && } 66 | {this.props.chartType == 'Pie' && } 67 | {this.props.chartType == 'Doughnut' && } 68 |
69 | 70 |
71 | 72 | {this.state.loading ? strings.Loading : strings.Refresh} 73 | 74 |
75 |
76 | ); 77 | } 78 | 79 | public componentDidMount(): void { 80 | this.getItems(); 81 | } 82 | 83 | public getItems(): void { 84 | this.setState({ loading: true }); 85 | 86 | SharePointService.getListItems(this.props.listId).then(items => { 87 | this.setState({ 88 | items: items.value, 89 | loading: false, 90 | error: null, 91 | }); 92 | }).catch(error => { 93 | this.setState({ 94 | error: strings.Error, 95 | loading: false, 96 | }); 97 | }); 98 | } 99 | 100 | public chartData(): object { 101 | // Chart data 102 | const data = { 103 | labels: [], 104 | datasets: [], 105 | }; 106 | 107 | // Add datasets 108 | this.state.items.map((item, i) => { 109 | // Create dataset 110 | const dataset = { 111 | label: '', 112 | data: [], 113 | backgroundColor: this.props.colors[i % this.props.colors.length], 114 | borderColor: this.props.colors[i % this.props.colors.length], 115 | }; 116 | 117 | // Build dataset 118 | this.props.selectedFields.map((field, j) => { 119 | // Get the value 120 | let value = item[field]; 121 | if (value === undefined && item[`OData_${field}`] !== undefined) { 122 | value = item[`OData_${field}`]; 123 | } 124 | 125 | // Add labels 126 | if (i == 0 && j > 0) { 127 | data.labels.push(field); 128 | } 129 | 130 | if (j == 0) { 131 | dataset.label = value; 132 | } else { 133 | dataset.data.push(value); 134 | } 135 | }); 136 | 137 | // Line chart 138 | if (this.props.chartType == 'Line') { 139 | dataset['fill'] = false; 140 | } 141 | 142 | data.datasets.push(dataset); 143 | }); 144 | 145 | return data; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/webparts/dash/components/Dash.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IDashProps } from './IDashProps'; 3 | import Chart from './Chart'; 4 | import { MessageBar } from 'office-ui-fabric-react/lib/MessageBar'; 5 | import * as strings from 'DashWebPartStrings'; 6 | 7 | export default class Dash extends React.Component { 8 | public render(): React.ReactElement { 9 | return ( 10 |
11 | {this.props.listId && this.props.selectedFields.length ? 12 | : 18 | 19 | {strings.Intro} 20 | } 21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/webparts/dash/components/IDashProps.ts: -------------------------------------------------------------------------------- 1 | export interface IDashProps { 2 | listId: string; 3 | selectedFields: string[]; 4 | chartType: string; 5 | chartTitle: string; 6 | colors: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/webparts/dash/loc/en-us.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | return { 3 | "AddColor": "Add Color", 4 | "ChartBar": "Bar", 5 | "ChartBarHorizontal": "Bar (Horizontal)", 6 | "ChartData": "Chart Data", 7 | "ChartDonut": "Donut", 8 | "ChartLine": "Line", 9 | "ChartPie": "Pie", 10 | "ChartSettings": "Chart Settings", 11 | "ChartStyle": "Chart Style", 12 | "ChartTitle": "Chart Title", 13 | "ChartType": "Chart Type", 14 | "Colors": "Colors", 15 | "DeleteColor": "Delete", 16 | "Error": "Something went wrong!", 17 | "Intro": "Select a list to continue...", 18 | "List": "List", 19 | "Loading": "Loading...", 20 | "LoadingChartData": "Loading chart data...", 21 | "PropertyPaneDescription": "Dash Settings", 22 | "Refresh": "Refresh", 23 | "SelectedFields": "Selected Fields", 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/webparts/dash/loc/mystrings.d.ts: -------------------------------------------------------------------------------- 1 | declare interface IDashWebPartStrings { 2 | AddColor: string; 3 | ChartBar: string; 4 | ChartBarHorizontal: string; 5 | ChartData: string; 6 | ChartDonut: string; 7 | ChartLine: string; 8 | ChartPie: string; 9 | ChartSettings: string; 10 | ChartStyle: string; 11 | ChartTitle: string; 12 | ChartType: string; 13 | Colors: string; 14 | DeleteColor: string; 15 | Error: string; 16 | Intro: string; 17 | List: string; 18 | Loading: string; 19 | LoadingChartData: string; 20 | PropertyPaneDescription: string; 21 | Refresh: string; 22 | SelectedFields: string; 23 | } 24 | 25 | declare module 'DashWebPartStrings' { 26 | const strings: IDashWebPartStrings; 27 | export = strings; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "experimentalDecorators": true, 11 | "skipLibCheck": true, 12 | "outDir": "lib", 13 | "typeRoots": [ 14 | "./node_modules/@types", 15 | "./node_modules/@microsoft" 16 | ], 17 | "types": [ 18 | "es6-promise", 19 | "webpack-env" 20 | ], 21 | "lib": [ 22 | "es5", 23 | "dom", 24 | "es2015.collection" 25 | ] 26 | }, 27 | "include": [ 28 | "src/**/*.ts" 29 | ], 30 | "exclude": [ 31 | "node_modules", 32 | "lib" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "tslint-microsoft-contrib" 4 | ], 5 | "rules": { 6 | "class-name": false, 7 | "export-name": false, 8 | "forin": false, 9 | "label-position": false, 10 | "member-access": true, 11 | "no-arg": false, 12 | "no-console": false, 13 | "no-construct": false, 14 | "no-duplicate-variable": true, 15 | "no-eval": false, 16 | "no-function-expression": true, 17 | "no-internal-module": true, 18 | "no-shadowed-variable": true, 19 | "no-switch-case-fall-through": true, 20 | "no-unnecessary-semicolons": true, 21 | "no-unused-expression": true, 22 | "no-use-before-declare": true, 23 | "no-with-statement": true, 24 | "semicolon": true, 25 | "trailing-comma": false, 26 | "typedef": false, 27 | "typedef-whitespace": false, 28 | "use-named-parameter": true, 29 | "variable-name": false, 30 | "whitespace": false 31 | } 32 | } --------------------------------------------------------------------------------