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:
parent
87e30dd02a
commit
13954cc7bc
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.4.1",
|
||||
"libraryName": "sp-fx-dynamic-grid",
|
||||
"libraryId": "23cfb83a-b316-4112-8f37-5c769a66c024",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -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" />
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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);
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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)
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
import { SPHttpClient } from '@microsoft/sp-http';
|
||||
|
||||
export interface IPurchaseRequestWebpartProps {
|
||||
description: string;
|
||||
siteUrl:string;
|
||||
spHttpClient:SPHttpClient;
|
||||
itemId:string;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
10
samples/react-reduxform/src/webparts/PurchaseRequestWebpart/loc/mystrings.d.ts
vendored
Normal file
10
samples/react-reduxform/src/webparts/PurchaseRequestWebpart/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare interface IDynamicGridWebpartWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'DynamicGridWebpartWebPartStrings' {
|
||||
const strings: IDynamicGridWebpartWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>;
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue