Merge pull request #1372 from Harshagracy/master

This commit is contained in:
Hugo Bernier 2020-07-07 22:41:54 -04:00 committed by GitHub
commit 9023f2335b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 8590 additions and 7319 deletions

View File

@ -2,7 +2,7 @@
"@microsoft/generator-sharepoint": { "@microsoft/generator-sharepoint": {
"isCreatingSolution": true, "isCreatingSolution": true,
"environment": "spo", "environment": "spo",
"version": "1.9.1", "version": "1.10.0",
"libraryName": "react-form-webpart", "libraryName": "react-form-webpart",
"libraryId": "b092661d-5730-49ea-be27-14ee4a84eb33", "libraryId": "b092661d-5730-49ea-be27-14ee4a84eb33",
"packageManager": "npm", "packageManager": "npm",

View File

@ -1,51 +1,57 @@
--- ---
page_type: sample page_type: sample
products: products:
- office-sp - office-sp
languages: languages:
- javascript - javascript
- typescript - typescript
extensions: extensions:
contentType: samples contentType: samples
technologies: technologies:
- SharePoint Framework - SharePoint Framework
platforms: platforms:
- react - react
createdDate: 12/1/2017 12:00:00 AM createdDate: 12/1/2017 12:00:00 AM
--- ---
# React List Form WebPart # React List Form WebPart
## Summary ## Summary
The `React List Form web part` is a web part for adding a list form to any page. It provides a working example of implementing generic SharePoint list forms using the **SharePoint Framework (SPFx)** and the *React* and *Office UI Fabric* libraries.
The `React List Form web part` is a web part for adding a list form to any page. It provides a working example of implementing generic SharePoint list forms using the **SharePoint Framework (SPFx)** and the _React_ and _Office UI Fabric_ libraries.
The web part allows configuring which list to use and if a form for adding a new item, editing or displaying an existing item should be shown. When selecting display or edit form the ID can be defined either as a fixed number or as a query string parameter name. The form fields can be added, ordered using drag-and-drop or removed visually in the web part. A URL including placeholder for the ID can be provided to redirect to after successfully saving the form. The web part allows configuring which list to use and if a form for adding a new item, editing or displaying an existing item should be shown. When selecting display or edit form the ID can be defined either as a fixed number or as a query string parameter name. The form fields can be added, ordered using drag-and-drop or removed visually in the web part. A URL including placeholder for the ID can be provided to redirect to after successfully saving the form.
![Demo](./assets/React-ListForm-Overview.gif) ![Demo](./assets/React-ListForm-Overview.gif)
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.9.1-green.svg)
![SPFx 1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to ## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview) - [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment) - [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## Solution ## Solution
Solution|Author(s) | Solution | Author(s) |
--------|--------- | --------------- | ----------------------------------------------------------------- |
react-list-form|Dany Wyss | react-list-form | Dany Wyss |
| react-list-form | Harsha Vardhini ([@harshagracy](https://twitter.com/harshagracy)) |
## Version history ## Version history
Version|Date|Comments | Version | Date | Comments |
-------|----|-------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------------- |
1.0.0|November 24, 2017|Initial release | 1.0.0 | November 24, 2017 | Initial release |
1.0.1|February 22, 2019|Updated to SPFx 1.7.1 and dependencies, Added Turkish translation, Added RichText Mode and Tinymce Editor | 1.0.1 | February 22, 2019 | Updated to SPFx 1.7.1 and dependencies, Added Turkish translation, Added RichText Mode and Tinymce Editor |
1.0.2|October 14, 2019|Updated to SPFx 1.9.1 and dependencies | 1.0.2 | October 14, 2019 | Updated to SPFx 1.9.1 and dependencies |
| 1.0.3 | July 7, 2020 | Updated to SPFx 1.10.0 and dependencies. Fixed required field validation (Harsha Vardhini) |
## Disclaimer ## 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.**
**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.**
--- ---

View File

@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "react-form-webpart-client-side-solution", "name": "react-form-webpart-client-side-solution",
"id": "373a20ef-dfc6-456a-95ec-171de3c94581", "id": "373a20ef-dfc6-456a-95ec-171de3c94581",
"version": "1.0.2.0", "version": "1.0.3.0",
"title": "List form", "title": "List form",
"supportedLocales": [ "supportedLocales": [
"en-US", "en-US",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "react-form-webpart", "name": "react-form-webpart",
"version": "1.0.2", "version": "1.0.3",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -13,16 +13,18 @@
"test": "gulp test" "test": "gulp test"
}, },
"dependencies": { "dependencies": {
"@microsoft/sp-core-library": "1.9.1", "@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.9.1", "@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.9.1", "@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-webpart-base": "1.9.1", "@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@tinymce/tinymce-react": "^3.0.1", "@tinymce/tinymce-react": "^3.0.1",
"@types/es6-promise": "0.0.33", "@types/es6-promise": "0.0.33",
"@types/react-dnd": "~2.0.34", "@types/react-dnd": "~2.0.34",
"@types/webpack-env": "1.13.1", "@types/webpack-env": "1.13.1",
"@uifabric/icons": "^7.3.14", "@uifabric/icons": "^7.3.14",
"moment": "^2.24.0", "moment": "^2.24.0",
"office-ui-fabric-react": "^6.189.2",
"react-dnd": "~2.5.4", "react-dnd": "~2.5.4",
"react-dnd-html5-backend": "~2.5.4", "react-dnd-html5-backend": "~2.5.4",
"react-html-parser": "^2.0.2", "react-html-parser": "^2.0.2",
@ -31,10 +33,11 @@
}, },
"devDependencies": { "devDependencies": {
"@microsoft/rush-stack-compiler-2.9": "^0.8.5", "@microsoft/rush-stack-compiler-2.9": "^0.8.5",
"@microsoft/sp-build-web": "1.9.1", "@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/sp-module-interfaces": "1.9.1", "@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-tslint-rules": "1.9.1", "@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.9.1", "@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@types/chai": "3.4.34", "@types/chai": "3.4.34",
"@types/mocha": "2.2.38", "@types/mocha": "2.2.38",
"ajv": "~5.2.2", "ajv": "~5.2.2",

View File

@ -11,4 +11,6 @@ export interface IListFormState {
notifications: string[]; notifications: string[];
fieldErrors: { [fieldName: string]: string }; fieldErrors: { [fieldName: string]: string };
showUnsupportedFields?: boolean; showUnsupportedFields?: boolean;
hasError: boolean;
errorInfo: string;
} }

View File

@ -47,13 +47,19 @@ class ListForm extends React.Component<IListFormProps, IListFormState> {
originalData: {}, originalData: {},
errors: [], errors: [],
notifications: [], notifications: [],
fieldErrors: {} fieldErrors: {},
hasError: false,
errorInfo: ''
}; };
this.listFormService = new ListFormService(props.spHttpClient); this.listFormService = new ListFormService(props.spHttpClient);
} }
public render() { public render() {
let menuProps; let menuProps;
if (this.state.hasError) {
// render any custom fallback UI
return <h1>{this.state.errorInfo}</h1>;
}
if (this.state.fieldsSchema) { if (this.state.fieldsSchema) {
menuProps = { menuProps = {
shouldFocusOnMount: true, shouldFocusOnMount: true,
@ -108,6 +114,12 @@ class ListForm extends React.Component<IListFormProps, IListFormState> {
); );
} }
public componentDidCatch(error, errorInfo) {
this.setState({
hasError: true,
errorInfo: error.toString()
});
}
private renderNotifications() { private renderNotifications() {
if (this.state.notifications.length === 0) { if (this.state.notifications.length === 0) {
return null; return null;
@ -272,62 +284,86 @@ class ListForm extends React.Component<IListFormProps, IListFormState> {
); );
} }
private async saveItem(): Promise<void> { private validator = () => {
this.setState({ ...this.state, isSaving: true, errors: [] }); let fieldErrors = this.state.fieldErrors;
try { this.state.fieldsSchema.forEach(currentFieldSchema => {
let updatedValues; if (currentFieldSchema.Required && !this.state.data[currentFieldSchema.InternalName]) {
if (this.props.id) { fieldErrors = {
updatedValues = await this.listFormService.updateItem( ...fieldErrors,
this.props.webUrl, [currentFieldSchema.InternalName]: strings.RequiredValueMessage
this.props.listUrl, };
this.props.id,
this.state.fieldsSchema,
this.state.data,
this.state.originalData);
} else {
updatedValues = await this.listFormService.createItem(
this.props.webUrl,
this.props.listUrl,
this.state.fieldsSchema,
this.state.data);
} }
let dataReloadNeeded = false; });
const newState: IListFormState = { ...this.state, fieldErrors: {} }; this.setState({
let hadErrors = false; fieldErrors: fieldErrors
updatedValues.filter((fieldVal) => fieldVal.HasException).forEach((element) => { });
newState.fieldErrors[element.FieldName] = element.ErrorMessage; for (let key in fieldErrors) {
hadErrors = true; if (fieldErrors[key]) {
}); return false;
if (hadErrors) {
if (this.props.onSubmitFailed) {
this.props.onSubmitFailed(newState.fieldErrors);
} else {
newState.errors = [...newState.errors, strings.FieldsErrorOnSaving];
}
} else {
updatedValues.reduce(
(val, merged) => {
merged[val.FieldName] = merged[val.FieldValue]; return merged;
},
newState.data,
);
// we shallow clone here, so that changing values on state.data won't be changing in state.originalData too
newState.originalData = { ...newState.data };
let id = (this.props.id) ? this.props.id : 0;
if (id === 0) {
id = updatedValues.filter((val) => val.FieldName === 'Id')[0].FieldValue;
}
if (this.props.onSubmitSucceeded) { this.props.onSubmitSucceeded(id); }
newState.notifications = [...newState.notifications, strings.ItemSavedSuccessfully];
dataReloadNeeded = true;
} }
newState.isSaving = false; }
this.setState(newState); return true;
}
if (dataReloadNeeded) { this.readData(this.props.listUrl, this.props.formType, this.props.id); } private async saveItem(): Promise<void> {
} catch (error) { let shouldSave = this.validator();
const errorText = strings.ErrorOnSavingListItem + error; if (shouldSave) {
this.setState({ ...this.state, errors: [...this.state.errors, errorText] }); this.setState({ ...this.state, isSaving: true, errors: [] });
try {
let updatedValues;
if (this.props.id) {
updatedValues = await this.listFormService.updateItem(
this.props.webUrl,
this.props.listUrl,
this.props.id,
this.state.fieldsSchema,
this.state.data,
this.state.originalData);
} else {
updatedValues = await this.listFormService.createItem(
this.props.webUrl,
this.props.listUrl,
this.state.fieldsSchema,
this.state.data);
}
let dataReloadNeeded = false;
const newState: IListFormState = { ...this.state, fieldErrors: {} };
let hadErrors = false;
updatedValues.filter((fieldVal) => fieldVal.HasException).forEach((element) => {
newState.fieldErrors[element.FieldName] = element.ErrorMessage;
hadErrors = true;
});
if (hadErrors) {
if (this.props.onSubmitFailed) {
this.props.onSubmitFailed(newState.fieldErrors);
} else {
newState.errors = [...newState.errors, strings.FieldsErrorOnSaving];
}
} else {
updatedValues.reduce(
(val, merged) => {
merged[val.FieldName] = merged[val.FieldValue]; return merged;
},
newState.data,
);
// we shallow clone here, so that changing values on state.data won't be changing in state.originalData too
newState.originalData = { ...newState.data };
let id = (this.props.id) ? this.props.id : 0;
if (id === 0) {
id = updatedValues.filter((val) => val.FieldName === 'Id')[0].FieldValue;
}
if (this.props.onSubmitSucceeded) { this.props.onSubmitSucceeded(id); }
newState.notifications = [...newState.notifications, strings.ItemSavedSuccessfully];
dataReloadNeeded = true;
}
newState.isSaving = false;
this.setState(newState);
if (dataReloadNeeded) { this.readData(this.props.listUrl, this.props.formType, this.props.id); }
} catch (error) {
const errorText = strings.ErrorOnSavingListItem + error;
this.setState({ ...this.state, errors: [...this.state.errors, errorText] });
}
} }
} }

View File

@ -11,7 +11,6 @@ import { ControlMode } from '../../../../common/datatypes/ControlMode';
import { IFieldSchema } from '../../../../common/services/datatypes/RenderListData'; import { IFieldSchema } from '../../../../common/services/datatypes/RenderListData';
import * as stylesImport from 'office-ui-fabric-react/lib/components/TextField/TextField.types'; import * as stylesImport from 'office-ui-fabric-react/lib/components/TextField/TextField.types';
const styles: any = stylesImport;
import ardStyles from './FormField.module.scss'; import ardStyles from './FormField.module.scss';
@ -29,7 +28,7 @@ export interface IFormFieldProps {
} }
const FormField: React.SFC<IFormFieldProps> = (props) => { const FormField: React.SFC<IFormFieldProps> = (props) => {
const styles: any = stylesImport;
const { const {
children, children,
className, className,
@ -60,13 +59,15 @@ const FormField: React.SFC<IFormFieldProps> = (props) => {
<span> <span>
{description && <span className={css('ard-FormField-description', styles.description)}>{description}</span>} {description && <span className={css('ard-FormField-description', styles.description)}>{description}</span>}
{errorMessage && {errorMessage &&
<div aria-live='assertive'> <div aria-live='assertive' style={{ color: '#a80000' }}>
<DelayedRender> {errorMessage}
<p className={css('ard-FormField-errorMessage', AnimationClassNames.slideDownIn20, styles.errorMessage)}> {/* <DelayedRender>
{Icon({ iconName: 'Error', className: styles.errorIcon })} <p className={css('ard-FormField-errorMessage', AnimationClassNames.slideDownIn20, styles.errorMessage ? styles.errorMessage : '')}>
<span className={styles.errorText} data-automation-id='error-message'>{errorMessage}</span> {Icon({ iconName: 'Error', className: styles.errorIcon ? styles.errorIcon : '' })}
<span className={styles.errorText ? styles.errorText : ''} data-automation-id='error-message'>{errorMessage}</span>
</p> </p>
</DelayedRender>
</DelayedRender> */}
</div> </div>
} }
</span> </span>

View File

@ -1,5 +1,5 @@
{ {
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json", "extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,