From 53210a56e916cec55307ccdadc9145ee67bb284a Mon Sep 17 00:00:00 2001 From: Ryan Schouten Date: Thu, 8 Oct 2020 22:12:33 -0700 Subject: [PATCH] Add cascading lookup support --- samples/react-list-form/README.md | 4 +- samples/react-list-form/package.json | 2 +- .../react-list-form/src/common/SPHelper.ts | 18 +++ .../src/common/services/IListFormService.ts | 2 + .../src/common/services/ListFormService.ts | 41 +++++- .../src/webparts/listForm/ListFormWebPart.ts | 4 +- .../webparts/listForm/components/ListForm.tsx | 117 +++++++++++++++--- .../formFields/SPFieldLookupEdit.tsx | 27 +++- .../formFields/SPFieldTaxonomyEdit.tsx | 2 +- 9 files changed, 193 insertions(+), 24 deletions(-) create mode 100644 samples/react-list-form/src/common/SPHelper.ts diff --git a/samples/react-list-form/README.md b/samples/react-list-form/README.md index d75042d24..c2dd64e9a 100755 --- a/samples/react-list-form/README.md +++ b/samples/react-list-form/README.md @@ -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 diff --git a/samples/react-list-form/package.json b/samples/react-list-form/package.json index 52a6ed74b..603d61e56 100755 --- a/samples/react-list-form/package.json +++ b/samples/react-list-form/package.json @@ -1,6 +1,6 @@ { "name": "react-form-webpart", - "version": "1.0.5", + "version": "1.0.6", "private": true, "engines": { "node": ">=0.10.0" diff --git a/samples/react-list-form/src/common/SPHelper.ts b/samples/react-list-form/src/common/SPHelper.ts new file mode 100644 index 000000000..d1e3250a5 --- /dev/null +++ b/samples/react-list-form/src/common/SPHelper.ts @@ -0,0 +1,18 @@ +export class SPHelper { + public static LookupValueToString(value: any | Array): string { + return value.map((item) => { return `${item.key};#${item.text}`; }).join(";#"); + } + public static LookupValueFromString(value: string): Array { + 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; + } + } +} \ No newline at end of file diff --git a/samples/react-list-form/src/common/services/IListFormService.ts b/samples/react-list-form/src/common/services/IListFormService.ts index 785ff1e62..b8c5d07c6 100644 --- a/samples/react-list-form/src/common/services/IListFormService.ts +++ b/samples/react-list-form/src/common/services/IListFormService.ts @@ -4,6 +4,8 @@ import { IWebPartContext } from '@microsoft/sp-webpart-base'; export interface IListFormService { getFieldSchemasForForm: (webUrl: string, listUrl: string, formType: ControlMode) => Promise; + getLookupfieldOptions: (fieldSchema: any, webUrl: string) => Promise; + getLookupfieldsOnList: (listUrl: string, webUrl: string, formType: ControlMode) => Promise; getDataForForm: (webUrl: string, listUrl: string, itemId: number, formType: ControlMode) => Promise; getExtraFieldData(data: any, fieldSchema: any, ctx: IWebPartContext, siteUrl: string); updateItem: (webUrl: string, listUrl: string, itemId: number, diff --git a/samples/react-list-form/src/common/services/ListFormService.ts b/samples/react-list-form/src/common/services/ListFormService.ts index 0dce40982..81a09080e 100644 --- a/samples/react-list-form/src/common/services/ListFormService.ts +++ b/samples/react-list-form/src/common/services/ListFormService.ts @@ -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 { + public async getFieldSchemasForForm(webUrl: string, listUrl: string, formType: ControlMode): Promise { return new Promise((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 { + 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 { + 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. * diff --git a/samples/react-list-form/src/webparts/listForm/ListFormWebPart.ts b/samples/react-list-form/src/webparts/listForm/ListFormWebPart.ts index 097ef6047..a04621184 100644 --- a/samples/react-list-form/src/webparts/listForm/ListFormWebPart.ts +++ b/samples/react-list-form/src/webparts/listForm/ListFormWebPart.ts @@ -36,7 +36,7 @@ export default class ListFormWebPart extends BaseClientSideWebPart>> 0; - var thisArg = arguments[1]; + var thisArg = argument; var value; for (var i = 0; i < length; i++) { diff --git a/samples/react-list-form/src/webparts/listForm/components/ListForm.tsx b/samples/react-list-form/src/webparts/listForm/components/ListForm.tsx index ff7047db1..7119b4a2a 100644 --- a/samples/react-list-form/src/webparts/listForm/components/ListForm.tsx +++ b/samples/react-list-form/src/webparts/listForm/components/ListForm.tsx @@ -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 { 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 { 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 { ); } 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 + : '' + } + }; + }, + ); + } } } diff --git a/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldLookupEdit.tsx b/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldLookupEdit.tsx index 9a89ab610..b665f3a45 100644 --- a/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldLookupEdit.tsx +++ b/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldLookupEdit.tsx @@ -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 = (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 = (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 , 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; diff --git a/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldTaxonomyEdit.tsx b/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldTaxonomyEdit.tsx index 8e5b09d77..d24f5717a 100644 --- a/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldTaxonomyEdit.tsx +++ b/samples/react-list-form/src/webparts/listForm/components/formFields/SPFieldTaxonomyEdit.tsx @@ -17,7 +17,7 @@ const SPFieldTaxonomyEdit: React.SFC = (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) => {