Merge pull request #1541 from sharepointknight/master

Add cascading lookup support
This commit is contained in:
Hugo Bernier 2020-10-12 20:26:17 -04:00 committed by GitHub
commit 976b458fb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 193 additions and 24 deletions

View File

@ -49,7 +49,9 @@ The web part allows configuring which list to use and if a form for adding a new
| 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.3 | July 7, 2020 | Updated to SPFx 1.10.0 and dependencies. Fixed required field validation (Harsha Vardhini) |
| 1.0.4 | September 12, 2020| Added support for User, UserMulti, Taxonomy, and TaxonomyMulti field types (Ryan Schouten) |
| 1.0.4 | September 12, 2020| Added support for User, UserMulti, Taxonomy, and TaxonomyMulti field types |
| 1.0.5 | September 26, 2020| Fix date handling problems and redirect after edit |
| 1.0.6 | October 8, 2020 | Added support for cascading lookup fields |
## Disclaimer

View File

@ -1,6 +1,6 @@
{
"name": "react-form-webpart",
"version": "1.0.5",
"version": "1.0.6",
"private": true,
"engines": {
"node": ">=0.10.0"

View File

@ -0,0 +1,18 @@
export class SPHelper {
public static LookupValueToString(value: any | Array<any>): string {
return value.map((item) => { return `${item.key};#${item.text}`; }).join(";#");
}
public static LookupValueFromString(value: string): Array<any> {
if (value == null) {
return [];
}
else {
const splitArray = value.split(';#');
let values = splitArray.filter((item, idx) => (idx % 2 === 0))
.map((comp, idx) => {
return { key: Number(comp), text: (splitArray.length >= idx * 2 + 1) ? splitArray[idx * 2 + 1] : '' };
});
return values;
}
}
}

View File

@ -4,6 +4,8 @@ import { IWebPartContext } from '@microsoft/sp-webpart-base';
export interface IListFormService {
getFieldSchemasForForm: (webUrl: string, listUrl: string, formType: ControlMode) => Promise<IFieldSchema[]>;
getLookupfieldOptions: (fieldSchema: any, webUrl: string) => Promise<any[]>;
getLookupfieldsOnList: (listUrl: string, webUrl: string, formType: ControlMode) => Promise<any[]>;
getDataForForm: (webUrl: string, listUrl: string, itemId: number, formType: ControlMode) => Promise<any>;
getExtraFieldData(data: any, fieldSchema: any, ctx: IWebPartContext, siteUrl: string);
updateItem: (webUrl: string, listUrl: string, itemId: number,

View File

@ -25,7 +25,7 @@ export class ListFormService implements IListFormService {
* @param formType The type of form (Display, New, Edit)
* @returns Promise object represents the array of field schema for all relevant fields for this list form.
*/
public getFieldSchemasForForm(webUrl: string, listUrl: string, formType: ControlMode): Promise<IFieldSchema[]> {
public async getFieldSchemasForForm(webUrl: string, listUrl: string, formType: ControlMode): Promise<IFieldSchema[]> {
return new Promise<IFieldSchema[]>((resolve, reject) => {
const httpClientOptions: ISPHttpClientOptions = {
headers: {
@ -63,7 +63,46 @@ export class ListFormService implements IListFormService {
});
});
}
/**
* Retrieves the options for a lookup field
*
* @param fieldSchema The field schema for the lookup field.
* @param webUrl The absolute Url to the SharePoint site.
* @returns Promise representing an object containing all the field values for the lookup field.
*/
public async getLookupfieldOptions(fieldSchema: any, webUrl: string): Promise<any[]> {
const endpoint = `${webUrl}/_api/Web/lists/getbyid('${fieldSchema.LookupListId}')/items?$orderby=${fieldSchema.LookupFieldName}`;
try {
let resp: SPHttpClientResponse = await this.spHttpClient.get(endpoint, SPHttpClient.configurations.v1);
if (resp.ok) {
let json = await resp.json();
return json.value.map((x) => {
return { LookupId: x.ID, LookupValue: x[fieldSchema.LookupFieldName], x };
});
}
}
catch (error) {
console.error(error);
}
return [];
}
/**
* Retrieves the options for a lookup field
*
* @param fieldSchema The field schema for the lookup field.
* @param webUrl The absolute Url to the SharePoint site.
* @returns Promise representing an object containing all the field values for the lookup field.
*/
public async getLookupfieldsOnList(listUrl: string, webUrl: string, formType: ControlMode): Promise<any[]> {
let fields = await this.getFieldSchemasForForm(webUrl, listUrl, formType);
fields = fields.filter((x) => {
return x.FieldType.indexOf("Lookup") === 0;
});
return fields;
}
/**
* Retrieves the data for a specified SharePoint list form.
*

View File

@ -36,7 +36,7 @@ export default class ListFormWebPart extends BaseClientSideWebPart<IListFormWebP
this.listService = new ListService(this.context.spHttpClient);
//Polyfill array find
if (!Array.prototype["find"]) {
Array.prototype["find"] = function (predicate) {
Array.prototype["find"] = function (predicate, argument) {
if (this == null) {
throw new TypeError('Array.prototype.find called on null or undefined');
}
@ -45,7 +45,7 @@ export default class ListFormWebPart extends BaseClientSideWebPart<IListFormWebP
}
var list = Object(this);
var length = list.length >>> 0;
var thisArg = arguments[1];
var thisArg = argument;
var value;
for (var i = 0; i < length; i++) {

View File

@ -10,6 +10,7 @@ import { ListFormService } from '../../../common/services/ListFormService';
import { ISPPeopleSearchService } from '../../../common/services/ISPPeopleSearchService';
import { SPPeopleSearchService } from '../../../common/services/SPPeopleSearchService';
import { GroupService } from '../../../common/services/GroupService';
import { SPHelper } from '../../../common/SPHelper';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
@ -244,6 +245,29 @@ class ListForm extends React.Component<IListFormProps, IListFormState> {
listUrl,
formType,
);
let lookupCount = 0;
for (let i = 0; i < fieldsSchema.length; i++) {
if (fieldsSchema[i].FieldType === "Lookup" || fieldsSchema[i].FieldType === "LookupMulti") {
fieldsSchema[i].Choices = await this.listFormService.getLookupfieldOptions(fieldsSchema[i], this.props.webUrl);
lookupCount += 1;
if (lookupCount > 1) {
let lookups = await this.listFormService.getLookupfieldsOnList(fieldsSchema[i]["LookupListUrl"], this.props.webUrl, formType);
for (let j = 0; j < lookups.length; j++) {
let parent = fieldsSchema.filter((x) => {
return x.LookupListId === lookups[j].LookupListId;
});
for (let k = 0; k < parent.length; k++) {
if (parent[k]["Dependent"] == null) {
parent[k]["Dependent"] = { Field: fieldsSchema[i], ValueField: lookups[j].InternalName };
break;
}
}
}
}
}
}
let userfields = fieldsSchema.filter((x) => {
return x.FieldType === "User" || x.FieldType === "UserMulti";
});
@ -281,9 +305,19 @@ class ListForm extends React.Component<IListFormProps, IListFormState> {
let dataObj = await this.listFormService.getDataForForm(this.props.webUrl, listUrl, id, formType);
const schema = this.state.fieldsSchema;
dataObj = await this.listFormService.getExtraFieldData(dataObj, schema, this.props.context, this.props.webUrl);
for (let i = 0; i < schema.length; i++) {
if (schema[i]["Dependent"] != null) {
let updateField = schema[i]["Dependent"].Field.InternalName;
let fieldsSchema = schema;
let dependee = fieldsSchema.filter((x) => {
return x.InternalName === updateField;
});
dependee[0]["DependerValue"] = { Value: dataObj[schema[i].InternalName], Field: schema[i]["Dependent"].ValueField };
}
}
// We shallow clone here, so that changing values on dataObj object fields won't be changing in originalData too
const dataObjOriginal = { ...dataObj };
this.setState({ ...this.state, data: dataObj, originalData: dataObjOriginal, isLoadingData: false });
this.setState({ ...this.state, data: dataObj, fieldsSchema: schema, originalData: dataObjOriginal, isLoadingData: false });
} catch (error) {
const errorText = `${strings.ErrorLoadingData}${id}: ${error}`;
this.setState({ ...this.state, data: {}, isLoadingData: false, errors: [...this.state.errors, errorText] });
@ -324,20 +358,75 @@ class ListForm extends React.Component<IListFormProps, IListFormState> {
);
}
else {
this.setState((prevState, props) => {
return {
...prevState,
data: { ...prevState.data, [fieldName]: newValue },
fieldErrors: {
...prevState.fieldErrors,
[fieldName]:
(prevState.fieldsSchema.filter((item) => item.InternalName === fieldName)[0].Required) && !newValue
? strings.RequiredValueMessage
: ''
// Check for if any other fields are dependent on this one
if (schema["Dependent"] != null) {
let dependee = [];
let fieldsSchema = this.state.fieldsSchema;
let updateField = schema["Dependent"].Field.InternalName;
let valueField = schema["Dependent"].ValueField;
let dependentValue = newValue;
do {
dependee = fieldsSchema.filter((x) => {
return x.InternalName === updateField;
});
dependee[0]["DependerValue"] = { Value: dependentValue, Field: valueField };
if (dependee.length > 0 && dependee[0]["Dependent"] != null) {
//Need to remove invalid options from dependent value
let tempVal = SPHelper.LookupValueFromString(this.state.data[dependee[0].InternalName]);
let depend = SPHelper.LookupValueFromString(dependentValue);
let choices = dependee[0].Choices;
let values = depend.map((item) => item.key);
choices = choices.filter((x) => {
let matches = values.filter((itm) => { return x.x[`${valueField}Id`] == itm; });
return matches.length > 0;
});
tempVal = tempVal.filter((x) => {
return choices.find((y) => { return y.LookupId == x.key; }) != null;
});
dependentValue = SPHelper.LookupValueToString(tempVal);
updateField = dependee[0]["Dependent"].Field.InternalName;
valueField = dependee[0]["Dependent"].ValueField;
}
};
},
);
else {
updateField = null;
}
} while (updateField != null);
this.setState((prevState, props) => {
return {
...prevState,
data: { ...prevState.data, [fieldName]: newValue },
fieldsSchema,
fieldErrors: {
...prevState.fieldErrors,
[fieldName]:
(prevState.fieldsSchema.filter((item) => item.InternalName === fieldName)[0].Required) && !newValue
? strings.RequiredValueMessage
: ''
}
};
},
);
}
else {
this.setState((prevState, props) => {
return {
...prevState,
data: { ...prevState.data, [fieldName]: newValue },
fieldErrors: {
...prevState.fieldErrors,
[fieldName]:
(prevState.fieldsSchema.filter((item) => item.InternalName === fieldName)[0].Required) && !newValue
? strings.RequiredValueMessage
: ''
}
};
},
);
}
}
}

View File

@ -2,15 +2,31 @@ import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { Dropdown, IDropdownProps, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import { SPHelper } from '../../../../common/SPHelper';
import * as strings from 'FormFieldStrings';
import styles from './SPFormField.module.scss';
const SPFieldLookupEdit: React.SFC<ISPFormFieldProps> = (props) => {
let options = props.fieldSchema.Choices.map((option) => ({ key: option.LookupId, text: option.LookupValue }));
let choices = props.fieldSchema.Choices;
if (props.fieldSchema["DependerValue"] != null) {
let parentField = props.fieldSchema["DependerValue"].Field;
let parentValue = props.fieldSchema["DependerValue"].Value;
const splitArray = parentValue.split(';#');
let values = splitArray.filter((item, idx) => (idx % 2 === 0));
let parentId = Number(parentValue.split(";#")[0]);
if (values.length > 0) {
choices = choices.filter((x) => {
let matches = values.filter((itm) => { return x.x[`${parentField}Id`] == itm; });
return matches.length > 0;
});
}
}
let options = choices.map((option) => ({ key: option.LookupId, text: option.LookupValue }));
if (props.fieldSchema.FieldType !== 'LookupMulti') {
if (!props.required) { options = [{ key: 0, text: strings.LookupEmptyOptionText }].concat(options); }
const value = props.value ? Number(props.value.split(';#')[0]) : 0;
return <Dropdown
className={css(styles.dropDownFormField, 'ard-lookupFormField')}
options={options}
@ -21,8 +37,11 @@ const SPFieldLookupEdit: React.SFC<ISPFormFieldProps> = (props) => {
let values = [];
if (props.value) {
const splitArray = props.value.split(';#');
values = splitArray.filter((item, idx) => (idx % 2 === 0))
.map((comp, idx) => ({ key: Number(comp), text: (splitArray.length > idx + 1) ? splitArray[idx + 1] : '' }));
values = SPHelper.LookupValueFromString(props.value);
//Need to remove invalid options for better cascading
values = values.filter((x) => {
return options.filter((option) => option.key == x.key).length > 0;
});
}
return <Dropdown
className={css(styles.dropDownFormField, 'ard-lookupMultiFormField')}
@ -42,7 +61,7 @@ function getUpdatedValue(oldValues: Array<{ key: number, text: string }>, change
} else {
newValues = oldValues.filter((item) => item.key !== changedItem.key);
}
return newValues.reduce((valStr, item) => valStr + `${item.key};#${item.text}`, '');
return SPHelper.LookupValueToString(newValues);
}
export default SPFieldLookupEdit;

View File

@ -17,7 +17,7 @@ const SPFieldTaxonomyEdit: React.SFC<ISPFormFieldProps> = (props) => {
context = props.context;
termsetId = props.fieldSchema.TermSetId;
allowMultipleSelections = props.fieldSchema.AllowMultipleValues;
if (props.value != null) {
if (props.value != null && props.value != "") {
terms = [];
let multiparts = props.value.split(";");
multiparts.forEach((x) => {