Redux-Form sample webpart (#503)

* Redux-Form sample SPFx webpart added.

New SPFx webpart sample added that uses redux-form library. Sample is a data entry form with dynamic grid built using typescript, react, redux and redux-form library.

* Readme file updated.

Readme file updated with correct webpart demo gif file path.
This commit is contained in:
Vipul Kelkar 2018-05-12 07:27:03 +05:30 committed by Vesa Juvonen
parent 87e30dd02a
commit 13954cc7bc
32 changed files with 18548 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-reduxform/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
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

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "sp-fx-dynamic-grid",
"libraryId": "23cfb83a-b316-4112-8f37-5c769a66c024",
"environment": "spo"
}
}

View File

@ -0,0 +1,73 @@
# SPFx webpart using Redux-Form library and React
## Summary
Sample webpart to demonstrate the use of [Redux-Form](https://github.com/erikras/redux-form) library with SPFx, React and Typescript. Demonstrates how to easily build a dynamic grid using redux-form.
![SPFx redux-form webpart](https://github.com/vipulkelkar/sp-dev-fx-webparts/blob/ReduxFormSample/samples/react-reduxform/assets/ReduxFormWebpart.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
- Basic knowledge of react-redux concepts - reducer,actions and dispatch.
- PnP PowerShell - to setup Fields and Lists to work with the webpart.
## Solution
Solution|Author(s)
--------|---------
react-reduxform | Vipul Kelkar @vipulkelkar
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 02, 2018|Initial release
## 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.**
---
## Minimal Path to Awesome
- The webpart requires two custom lists in the SharePoint site. The folder "SetupScript" contains a PnP PowerShell script that will setup the custom fields, content type and the required lists.
- Change the site URL in the PowerShell script and execute with proper credentials to setup the lists.
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
- Navigate to - <Your SP site>/_layouts/workbench.aspx and add the "ReduxFormWebpart"
## Features
- This is a data entry webpart to create a "Purchase Request". Each purchase request can have several items that can be ordered within it.
- The webpart interacts with two SharePoint lists - "PurchaseRequest" and "PurchaseRequestItems".
- The purchase request is created in the "PurchaseRequest" list. The purchase items are stored in separate "PurchaseRequestItems" list along with the ListItem ID of the corrosponding PurchaseRequest item.
Redux-Form :
- Webpart makes use of [Redux-Form](https://github.com/erikras/redux-form) library which makes it easy to maintain the state of form fields without explicitly requiring to maintain the state everytime data in a form field is added/updated/deleted.
Refer to the [Getting started](https://redux-form.com/6.4.3/docs/gettingstarted.md/) guide for the basics of redux-form
- The sample demonstrates how to integrate redux-form in SPFx webpart and develop a dynamic grid component using [FieldArray](https://redux-form.com/6.4.3/docs/api/fieldarray.md/) component of redux-form.
- The sample also uses custom renderers for form fields along with required field and number validations.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-reduxform" />

View File

@ -0,0 +1,59 @@
Connect-PnPOnline -Url <URL>
Write-Host "Starting the setup....."
$fieldAndCTGroup = "ReduxFormSample"
Write-Host "Creating fields....."
Add-PnPField -DisplayName ProductCode -InternalName ProductCode -Type Text -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
Add-PnPField -DisplayName PurchasedFor -InternalName PurchasedFor -Type Choice -Choices IT,Admin,HR,Finance -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
Add-PnPField -DisplayName TypeOfPR -InternalName TypeOfPR -Type Choice -Choices @("Maintenance","IT Asset Purchase","Stationary","Other Services") -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
Add-PnPField -DisplayName PurchaseRequestID -InternalName PurchaseRequestID -Type Number -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
Add-PnPField -DisplayName Quantity -InternalName Quantity -Type Number -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
Add-PnPField -DisplayName RatePerUnit -InternalName RatePerUnit -Type Number -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
Add-PnPField -DisplayName TotalCost -InternalName TotalCost -Type Number -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
Write-Host "Creating content types....."
Add-PnPContentType -Name 'PurchaseRequest' -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
$purchaseRequestCT = Get-PnPContentType -Identity 'PurchaseRequest'
Add-PnPContentType -Name 'PurchaseRequestItems' -Group $fieldAndCTGroup -ErrorAction SilentlyContinue
$purchaseRequestItemsCT = Get-PnPContentType -Identity 'PurchaseRequestItems'
Write-Host "Adding fields to content type....."
# Add columns to purchase request CT
Add-PnPFieldToContentType -Field PurchasedFor -ContentType $purchaseRequestCT
Add-PnPFieldToContentType -Field TypeOfPR -ContentType $purchaseRequestCT
# Add columns to purchase request items CT
Add-PnPFieldToContentType -Field ProductCode -ContentType $purchaseRequestItemsCT
Add-PnPFieldToContentType -Field Quantity -ContentType $purchaseRequestItemsCT
Add-PnPFieldToContentType -Field RatePerUnit -ContentType $purchaseRequestItemsCT
Add-PnPFieldToContentType -Field TotalCost -ContentType $purchaseRequestItemsCT
Add-PnPFieldToContentType -Field PurchaseRequestID -ContentType $purchaseRequestItemsCT
Write-Host "Creating lists....."
New-PnPList -Title 'PurchaseRequest' -Template GenericList -Url Lists/PurchaseRequest -ErrorAction SilentlyContinue
$purchaseRequestList = Get-PnPList -Identity Lists/PurchaseRequest
Set-PnPList -Identity 'PurchaseRequest' -EnableContentTypes $true
Add-PnPContentTypeToList -List $purchaseRequestList -ContentType $purchaseRequestCT -DefaultContentType
New-PnPList -Title 'PurchaseRequestItems' -Template GenericList -Url Lists/PurchaseRequestItems -ErrorAction SilentlyContinue
$purchaseRequestItemsList = Get-PnPList -Identity Lists/PurchaseRequestItems
Set-PnPList -Identity 'PurchaseRequestItems' -EnableContentTypes $true
Add-PnPContentTypeToList -List $purchaseRequestItemsList -ContentType $purchaseRequestItemsCT -DefaultContentType
Write-Host "Setting up list views....."
Add-PnPView -List $purchaseRequestList -Title PurchaseRequest -SetAsDefault -Fields PurchasedFor,TypeOfPR,Author
Add-PnPView -List $purchaseRequestItemsList -Title PurchaseRequestItems -SetAsDefault -Fields ProductCode,Quantity,RatePerUnit,TotalCost,PurchaseRequestID
Write-Host "Setup completed....."

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"dynamic-grid-webpart-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/PurchaseRequestWebpart/ReduxFormWebpart.js",
"manifest": "./src/webparts/PurchaseRequestWebpart/ReduxFormWebpartWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"DynamicGridWebpartWebPartStrings": "lib/webparts/PurchaseRequestWebpart/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "sp-fx-dynamic-grid",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "sp-fx-dynamic-grid-client-side-solution",
"id": "23cfb83a-b316-4112-8f37-5c769a66c024",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/sp-fx-dynamic-grid.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"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-case": true,
"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-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

7
samples/react-reduxform/gulpfile.js vendored Normal file
View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
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.`);
build.initialize(gulp);

17544
samples/react-reduxform/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"name": "sp-fx-dynamic-grid",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/redux-form": "^7.2.4",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"react": "15.6.2",
"react-dom": "15.6.2",
"react-redux": "^5.0.7",
"redux": "^4.0.0",
"redux-form": "^7.3.0",
"redux-thunk": "^2.2.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"sp-pnp-js": "^3.0.7"
}
}

View File

@ -0,0 +1,67 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'DynamicGridWebpartWebPartStrings';
import PurchaseRequestWebpart from './components/PurchaseRequestWebpart/PurchaseRequestWebpart';
import { IPurchaseRequestWebpartProps } from './components/PurchaseRequestWebpart/IPurchaseRequestWebpartProps';
import { UrlQueryParameterCollection } from '@microsoft/sp-core-library';
export interface IReduxFormWebpartProps {
description: string;
}
export default class ReduxFormWebpart extends BaseClientSideWebPart<IReduxFormWebpartProps> {
public render(): void {
var queryParameters = new UrlQueryParameterCollection(window.location.href);
let id: string = "";
if (queryParameters.getValue("itemid")) {
id = queryParameters.getValue("itemid");
}
const element: React.ReactElement<IReduxFormWebpartProps> = React.createElement(
PurchaseRequestWebpart,
{
description: this.properties.description,
siteUrl:this.context.pageContext.web.absoluteUrl,
spHttpClient:this.context.spHttpClient,
itemId:id
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "5eae71fd-ab9e-4c89-ac18-24d2e19e9e79",
"alias": "DynamicGridWebpartWebPart",
"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,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "ReduxFormWebpart" },
"description": { "default": "Sample webpart using redux-form library in SPFx." },
"officeFabricIconFontName": "Page",
"properties": {
"description": "Sample webpart using redux-form library in SPFx."
}
}]
}

View File

@ -0,0 +1,45 @@
import { INewFormState } from '../state/INewFormControlsState';
import NewPurchaseRequestService from '../services/NewPurchaseRequestService';
// The file contains actions for the NewPurchaseRequestReducer
// Gets the choices for dropdown fields in the new form. The values are fetched from the choice field options.
export function GetInitialControlValuesAction(){
return dispatch => {
let newPurchaseRequestServiceObj:NewPurchaseRequestService = new NewPurchaseRequestService();
let formControlsState = {purchasedForOptions:[],typeOfPurchaseRequestOptions:[]} as INewFormState;
newPurchaseRequestServiceObj.getNewFormControlsState().then((resp:INewFormState) => {
formControlsState.purchasedForOptions = resp.purchasedForOptions;
formControlsState.typeOfPurchaseRequestOptions = resp.typeOfPurchaseRequestOptions;
dispatch({
type:"GET_DEFAULT_CONTROL_VALUES",
payload:formControlsState
});
});
};
}
// Creates a new purchase request.
export function CreateNewPurchaseRequest(purchaseRequestData:INewFormState, siteUrl){
return dispatch => {
let newPurchaseRequestServiceObj:NewPurchaseRequestService = new NewPurchaseRequestService();
newPurchaseRequestServiceObj.createNewPurchaseRequest(purchaseRequestData,siteUrl).then(response =>{
alert("Purchase request created...");
}).catch(()=>{
alert("Error in creating purchase request...")
});
dispatch({
type:"CREATE_NEW_REQUEST",
payload:purchaseRequestData
});
};
}

View File

@ -0,0 +1,166 @@
import * as React from 'react';
import { INewFormState } from '../../state/INewFormControlsState';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { IPurchaseRequestWebpartProps } from '../PurchaseRequestWebpart/IPurchaseRequestWebpartProps';
import { GetInitialControlValuesAction, CreateNewPurchaseRequest } from '../../actions/NewFormControlsValuesAction';
import { Dropdown, IDropdown, DropdownMenuItemType, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { DefaultButton, IButtonProps } from 'office-ui-fabric-react/lib/Button';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Field, reduxForm, InjectedFormProps, FieldArray, WrappedFieldArrayProps, BaseFieldArrayProps } from 'redux-form';
import pnp from 'sp-pnp-js';
import { renderDropDown, renderInput } from '../Redux-Form-CustomComponents/FieldRenderers';
// Connected state
interface INewFormConnectedState{
// Represents a purchase request and the data from the form.
newFormControlValues : INewFormState;
// Represents the initial values. << Unused now. Useful for edit item feature >>
initialValues:any;
}
// Represents the connected dispatch
interface INewFormConnectedDispatch{
// Gets the options for dropdown fields
getDefaultControlsData:() => void;
createNewPurchaseRequest:(purchaseRequestData:INewFormState, siteUrl:string) => void;
}
// Validations for the redux form
const required = value => (value ? undefined : ' *');
const number = value =>
value && isNaN(Number(value)) ? ' Invalid value' : undefined;
// Represents the repeating purchase items component.
// Used in "NewRequestComponent" react component below along with "FieldsArray" from redux-form.
// Renders custom input component with validations
class PurchaseItemsComponent extends React.Component<WrappedFieldArrayProps<any>,{}>{
public render(){
return(
<div>
<button type="button" onClick={() => this.props.fields.push({})}>Add purchase item</button>
{this.props.fields.map((purchaseItem, index) =>
<tr>
<div>
<h4>Item {index + 1}</h4>
<td>
<Field name={`${purchaseItem}.productCode`} type="text" component={renderInput} placeholder="Product code" validate={[required]}/>
</td>
<td>
<Field name={`${purchaseItem}.quantity`} type="text" component={renderInput} placeholder="Quantity" validate={[required,number]}/>
</td>
<td>
<Field name={`${purchaseItem}.ratePerUnit`} type="text" component={renderInput} placeholder="Price per unit" validate={[required,number]}/>
</td>
<td>
<Field name={`${purchaseItem}.totalCost`} type="text" component={renderInput} placeholder="Total Cost" validate={[required,number]}/>
</td>
<td>
<button type="button" title="Remove Item" onClick={() => this.props.fields.remove(index)}>Remove item</button>
</td>
</div>
</tr>
)}
</div>
);
}
}
class NewRequestComponent extends React.Component<INewFormConnectedState & INewFormConnectedDispatch & IPurchaseRequestWebpartProps & InjectedFormProps<{}, INewFormConnectedState>>{
constructor(props){
super(props);
}
public render(){
return(
<div>
{/* Sent the props as well to the SubmitForm handler to use the Connected Dispatch. Renders custom dropdown component with validation*/}
<form onSubmit={this.props.handleSubmit(((values)=>this.SubmitForm(values,this.props)))}>
<div>
<Field component={renderDropDown} label="Purchased for : " name="purchasedFor" validate={required}>
<option key='' value=''></option>
{this.props.newFormControlValues.purchasedForOptions.map(purchasedFor => {return <option key={purchasedFor} value={purchasedFor}>{purchasedFor}</option>})};
</Field>
</div>
<br/>
<div>
<Field component={renderDropDown} label="Type of purchase request : " name="typeOfPurchaseRequest" validate={required}>
<option key='' value=''></option>
{this.props.newFormControlValues.typeOfPurchaseRequestOptions.map(typeOfPr => {return <option key={typeOfPr} value={typeOfPr}>{typeOfPr}</option>})} ;
</Field>
</div>
<br/>
<table>
<FieldArray name="purchaseItems" component={PurchaseItemsComponent}/>
</table>
<br/>
<button type="submit" disabled={this.props.submitting}>Create purchase request</button>
<br/>
</form>
</div>
);
}
// Handles the submit form.
SubmitForm(values, props){
let purchaseRequestData = {} as INewFormState;
purchaseRequestData = values;
purchaseRequestData.purchasedForOptions = props.newFormControlValues.purchasedForOptions;
purchaseRequestData.typeOfPurchaseRequestOptions = props.newFormControlValues.typeOfPurchaseRequestOptions;
// Call the connected dispatch to create new purchase request
props.createNewPurchaseRequest(purchaseRequestData,props.siteUrl);
}
componentDidMount(){
this.props.getDefaultControlsData();
}
}
// Maps the State to props
const mapStateToProps = (state) : INewFormConnectedState => {
// Includes the initialValues property to load the form with initial values
return{
newFormControlValues : state.NewFormControlValues,
initialValues : state.NewFormControlValues
};
};
// Maps dispatch to props
const mapDispatchToProps = (dispatch):INewFormConnectedDispatch => {
return{
getDefaultControlsData:() => {
return dispatch(GetInitialControlValuesAction());
},
createNewPurchaseRequest:(purchaseRequestData:INewFormState, siteUrl:string) => {
return dispatch(CreateNewPurchaseRequest(purchaseRequestData,siteUrl));
}
};
};
export default connect(mapStateToProps,mapDispatchToProps)(
reduxForm<{},INewFormConnectedState>(
{
form:'NewPurchaseRequestForm',
destroyOnUnmount:false,
// Reinitializes when the state changes. << Unused at the moment. Useful in edit item feature >>
enableReinitialize:true
}
)(NewRequestComponent)
);

View File

@ -0,0 +1,8 @@
import { SPHttpClient } from '@microsoft/sp-http';
export interface IPurchaseRequestWebpartProps {
description: string;
siteUrl:string;
spHttpClient:SPHttpClient;
itemId:string;
}

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.dynamicGridWebpart {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import styles from './PurchaseRequestWebpart.module.scss';
import { IPurchaseRequestWebpartProps } from './IPurchaseRequestWebpartProps';
import { escape } from '@microsoft/sp-lodash-subset';
import ConfigureStore from "../../store/ConfigureStore";
import { connect } from "react-redux";
import {INewFormState} from "../../state/INewFormControlsState";
import { Provider } from "react-redux";
import NewPurchaseRequestComponent from "../CreateNewRequest/CreateNewRequestComponent";
export default class PurchaseRequestWebpart extends React.Component<IPurchaseRequestWebpartProps, {}> {
public render(){
// Initialize the redux store
const purchaseRequertStore = ConfigureStore();
return (
<Provider store={purchaseRequertStore}>
<NewPurchaseRequestComponent {...this.props}/>
</Provider>
);
}
}

View File

@ -0,0 +1,37 @@
import * as React from 'react';
// The file contains custom field render components used in the redux form in the purchase requestwebpart.
var requiredMessageStyle={
color:'Red'
};
// Custom renderer for dropdown field with validation message
export class renderDropDown extends React.Component<any,{}>{
render() {
return (
<div>
<label>{this.props.label}</label>
<select {...this.props.input}>
{this.props.children}
</select>
<br/>
{this.props.meta.touched && this.props.meta.error && <span style={requiredMessageStyle}>{this.props.meta.error}</span>}
</div>
);
}
}
// Custom renderer for Input fields with validation message
export class renderInput extends React.Component<any,{}>{
render() {
return (
<div>
<label>{this.props.label}</label>
<input {...this.props.input} placeholder={this.props.placeholder}></input>
<br/>
{this.props.meta.touched && this.props.meta.error && <span style={requiredMessageStyle}>{this.props.meta.error}</span>}
</div>
);
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IDynamicGridWebpartWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'DynamicGridWebpartWebPartStrings' {
const strings: IDynamicGridWebpartWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,45 @@
import { INewFormState } from '../state/INewFormControlsState';
import { GetInitialControlValuesAction } from '../actions/NewFormControlsValuesAction';
// Initial state of the purcahse request.
export const newFormControlsInitialState:INewFormState = {
purchasedFor:"",
typeOfPurchaseRequest:"",
purchasedForOptions:[],
typeOfPurchaseRequestOptions:[],
purchaseItems:[]
};
export const NewPurchaseRequestReducer = (state:INewFormState=newFormControlsInitialState,action) => {
switch(action.type){
// Gets the values for dropdown fields from SharePoint choice columns.
case "GET_DEFAULT_CONTROL_VALUES":
state={
...state,
purchasedForOptions : action.payload.purchasedForOptions,
typeOfPurchaseRequestOptions : action.payload.typeOfPurchaseRequestOptions,
};
break;
// Creates a new purchase request.
case "CREATE_NEW_REQUEST":
state={
...state,
purchasedFor : action.payload.purchasedFor,
typeOfPurchaseRequest : action.payload.typeOfPurchaseRequest,
purchasedForOptions : action.payload.purchasedForOptions,
typeOfPurchaseRequestOptions : action.payload.typeOfPurchaseRequestOptions,
purchaseItems: action.payload.purchaseItems
};
break;
}
return state;
};

View File

@ -0,0 +1,7 @@
import { INewFormState } from '../state/INewFormControlsState';
// Represents the service to interact with SharePoint to work with purchase request.
export default interface INewPurchaseRequestService{
getNewFormControlsState() : Promise<any>;
createNewPurchaseRequest(purchaseRequestData:INewFormState,siteUrl) : Promise<any>;
}

View File

@ -0,0 +1,73 @@
import INewPurchaseRequestService from "./INewPurchaseRequestService";
import pnp from "sp-pnp-js";
import { INewFormState } from "../state/INewFormControlsState";
import { ItemAddResult,Web } from "sp-pnp-js";
export default class NewPurchaseRequestService implements INewPurchaseRequestService{
private getPurchasedForControlValues():Promise<any>{
return pnp.sp.web.fields.getByTitle("PurchasedFor").select("Choices").get().then(response => {
return response;
});
}
private getTypeOfPurchaseRequestValues():Promise<any>{
return pnp.sp.web.fields.getByTitle("TypeOfPR").select("Choices").get().then(response => {
return response;
});
}
// Gets the choices to be displayed in the dropdown fields.
getNewFormControlsState():Promise<any>{
let newFormControlsState = {} as INewFormState;
return this.getPurchasedForControlValues().then(purchasedForValuesResponse => {
newFormControlsState.purchasedForOptions = purchasedForValuesResponse.Choices;
return this.getTypeOfPurchaseRequestValues().then(typeOfPurchaseRequestResponse => {
newFormControlsState.typeOfPurchaseRequestOptions = typeOfPurchaseRequestResponse.Choices;
return newFormControlsState;
});
});
}
// Creates a new purchase request. The request is created in two list. One where the master data is stored and one
// where the purchase items are stored with a reference of the ID of master request.
async createNewPurchaseRequest(purchaseRequestData:INewFormState,siteUrl) : Promise<any>{
return pnp.sp.web.lists.getByTitle("PurchaseRequest").items.add({
PurchasedFor:purchaseRequestData.purchasedFor,
TypeOfPR:purchaseRequestData.typeOfPurchaseRequest})
.then((result:ItemAddResult)=>{
let purchaseRequestID = result.data.Id;
console.log("Purchase request created : " + purchaseRequestID);
if(purchaseRequestData.purchaseItems != null && purchaseRequestData.purchaseItems.length > 0){
// Creates the multiple purchase items in batch.
let web = new Web(siteUrl);
let batch = web.createBatch();
purchaseRequestData.purchaseItems.forEach(purchaseItem => {
web.lists.getByTitle("PurchaseRequestItems").items.inBatch(batch).add({
ProductCode:purchaseItem.productCode,
Quantity:purchaseItem.quantity,
RatePerUnit:purchaseItem.ratePerUnit,
TotalCost:purchaseItem.totalCost,
PurchaseRequestID:purchaseRequestID
});
});
batch.execute().then(()=>{
console.log("Purchase items added to the list....");
});
}
else{
alert('Select atleast one purchase item.');
}
});
}
}

View File

@ -0,0 +1,21 @@
// Represents a purchase request
export interface INewFormState{
// Represent the choices to be displayed in dropdown when the form loads.
purchasedForOptions:string[];
typeOfPurchaseRequestOptions:string[];
// Represent the values selected for the fields
purchasedFor:string;
typeOfPurchaseRequest:string;
purchaseItems:IPurchaseItem[];
}
// Represents one purchase item in the purchase request.
export interface IPurchaseItem{
productCode:string;
quantity:number;
ratePerUnit:number;
totalCost:number;
}

View File

@ -0,0 +1,22 @@
import { createStore,combineReducers, applyMiddleware } from "redux";
import { NewPurchaseRequestReducer } from "../reducers/NewPurchaseRequestReducer";
import thunk from "redux-thunk";
import { reducer as formReducer } from 'redux-form';
// Configures the redux store.
export default function ConfigureStore():any{
// Combine multiple reducers to create the store. FormReducer is for the redux-form.
const PurchaseRequestStore = createStore(
combineReducers
({
NewFormControlValues:NewPurchaseRequestReducer,
form:formReducer
}),
{},
applyMiddleware(thunk)
);
return PurchaseRequestStore;
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}