From a2f1aba6d9dd793816438984dc7ae95a5c80f15c Mon Sep 17 00:00:00 2001 From: Thomas German Date: Mon, 31 Jul 2023 01:56:26 +0100 Subject: [PATCH 1/3] Fix number validation issue, extend to currency --- src/controls/dynamicForm/CurrencyMap.ts | 249 ++++++++++++++++++ src/controls/dynamicForm/DynamicForm.tsx | 44 +++- src/controls/dynamicForm/IDynamicFormState.ts | 2 + .../dynamicForm/dynamicField/DynamicField.tsx | 45 +++- .../dynamicField/IDynamicFieldProps.ts | 1 + 5 files changed, 319 insertions(+), 22 deletions(-) create mode 100644 src/controls/dynamicForm/CurrencyMap.ts diff --git a/src/controls/dynamicForm/CurrencyMap.ts b/src/controls/dynamicForm/CurrencyMap.ts new file mode 100644 index 000000000..255ec0f9f --- /dev/null +++ b/src/controls/dynamicForm/CurrencyMap.ts @@ -0,0 +1,249 @@ +export default { + AD: 'EUR', + AE: 'AED', + AF: 'AFN', + AG: 'XCD', + AI: 'XCD', + AL: 'ALL', + AM: 'AMD', + AO: 'AOA', + AR: 'ARS', + AS: 'USD', + AT: 'EUR', + AU: 'AUD', + AW: 'AWG', + AX: 'EUR', + AZ: 'AZN', + BA: 'BAM', + BB: 'BBD', + BD: 'BDT', + BE: 'EUR', + BF: 'XOF', + BG: 'BGN', + BH: 'BHD', + BI: 'BIF', + BJ: 'XOF', + BL: 'EUR', + BM: 'BMD', + BN: 'BND', + BO: 'BOB', + BQ: 'USD', + BR: 'BRL', + BS: 'BSD', + BT: 'BTN', + BV: 'NOK', + BW: 'BWP', + BY: 'BYN', + BZ: 'BZD', + CA: 'CAD', + CC: 'AUD', + CD: 'CDF', + CF: 'XAF', + CG: 'XAF', + CH: 'CHF', + CI: 'XOF', + CK: 'NZD', + CL: 'CLP', + CM: 'XAF', + CN: 'CNY', + CO: 'COP', + CR: 'CRC', + CU: 'CUP', + CV: 'CVE', + CW: 'ANG', + CX: 'AUD', + CY: 'EUR', + CZ: 'CZK', + DE: 'EUR', + DJ: 'DJF', + DK: 'DKK', + DM: 'XCD', + DO: 'DOP', + DZ: 'DZD', + EC: 'USD', + EE: 'EUR', + EG: 'EGP', + EH: 'MAD', + ER: 'ERN', + ES: 'EUR', + ET: 'ETB', + FI: 'EUR', + FJ: 'FJD', + FK: 'FKP', + FM: 'USD', + FO: 'DKK', + FR: 'EUR', + GA: 'XAF', + GB: 'GBP', + GD: 'XCD', + GE: 'GEL', + GF: 'EUR', + GG: 'GBP', + GH: 'GHS', + GI: 'GIP', + GL: 'DKK', + GM: 'GMD', + GN: 'GNF', + GP: 'EUR', + GQ: 'XAF', + GR: 'EUR', + GS: 'GBP', + GT: 'GTQ', + GU: 'USD', + GW: 'XOF', + GY: 'GYD', + HK: 'HKD', + HM: 'AUD', + HN: 'HNL', + HR: 'EUR', + HT: 'HTG', + HU: 'HUF', + ID: 'IDR', + IE: 'EUR', + IL: 'ILS', + IM: 'GBP', + IN: 'INR', + IO: 'USD', + IQ: 'IQD', + IR: 'IRR', + IS: 'ISK', + IT: 'EUR', + JE: 'GBP', + JM: 'JMD', + JO: 'JOD', + JP: 'JPY', + KE: 'KES', + KG: 'KGS', + KH: 'KHR', + KI: 'AUD', + KM: 'KMF', + KN: 'XCD', + KP: 'KPW', + KR: 'KRW', + KW: 'KWD', + KY: 'KYD', + KZ: 'KZT', + LA: 'LAK', + LB: 'LBP', + LC: 'XCD', + LI: 'CHF', + LK: 'LKR', + LR: 'LRD', + LS: 'LSL', + LT: 'EUR', + LU: 'EUR', + LV: 'EUR', + LY: 'LYD', + MA: 'MAD', + MC: 'EUR', + MD: 'MDL', + ME: 'EUR', + MF: 'EUR', + MG: 'MGA', + MH: 'USD', + MK: 'MKD', + ML: 'XOF', + MM: 'MMK', + MN: 'MNT', + MO: 'MOP', + MP: 'USD', + MQ: 'EUR', + MR: 'MRO', + MS: 'XCD', + MT: 'EUR', + MU: 'MUR', + MV: 'MVR', + MW: 'MWK', + MX: 'MXN', + MY: 'MYR', + MZ: 'MZN', + NA: 'NAD', + NC: 'XPF', + NE: 'XOF', + NF: 'AUD', + NG: 'NGN', + NI: 'NIO', + NL: 'EUR', + NO: 'NOK', + NP: 'NPR', + NR: 'AUD', + NU: 'NZD', + NZ: 'NZD', + OM: 'OMR', + PA: 'PAB', + PE: 'PEN', + PF: 'XPF', + PG: 'PGK', + PH: 'PHP', + PK: 'PKR', + PL: 'PLN', + PM: 'EUR', + PN: 'NZD', + PR: 'USD', + PS: 'ILS', + PT: 'EUR', + PW: 'USD', + PY: 'PYG', + QA: 'QAR', + RE: 'EUR', + RO: 'RON', + RS: 'RSD', + RU: 'RUB', + RW: 'RWF', + SA: 'SAR', + SB: 'SBD', + SC: 'SCR', + SD: 'SDG', + SE: 'SEK', + SG: 'SGD', + SH: 'SHP', + SI: 'EUR', + SJ: 'NOK', + SK: 'EUR', + SL: 'SLL', + SM: 'EUR', + SN: 'XOF', + SO: 'SOS', + SR: 'SRD', + ST: 'STD', + SV: 'SVC', + SX: 'ANG', + SY: 'SYP', + SZ: 'SZL', + TC: 'USD', + TD: 'XAF', + TF: 'EUR', + TG: 'XOF', + TH: 'THB', + TJ: 'TJS', + TK: 'NZD', + TL: 'USD', + TM: 'TMT', + TN: 'TND', + TO: 'TOP', + TR: 'TRY', + TT: 'TTD', + TV: 'AUD', + TW: 'TWD', + TZ: 'TZS', + UA: 'UAH', + UG: 'UGX', + UM: 'USD', + US: 'USD', + UY: 'UYU', + UZ: 'UZS', + VA: 'EUR', + VC: 'XCD', + VE: 'VEF', + VG: 'USD', + VI: 'USD', + VN: 'VND', + VU: 'VUV', + WF: 'XPF', + WS: 'WST', + YE: 'YER', + YT: 'EUR', + ZA: 'ZAR', + ZM: 'ZMW', + ZW: 'ZWL' + }; \ No newline at end of file diff --git a/src/controls/dynamicForm/DynamicForm.tsx b/src/controls/dynamicForm/DynamicForm.tsx index 86a5a8b70..5ad9ddfbe 100644 --- a/src/controls/dynamicForm/DynamicForm.tsx +++ b/src/controls/dynamicForm/DynamicForm.tsx @@ -1,6 +1,6 @@ /* eslint-disable @microsoft/spfx/no-async-await */ import { SPHttpClient } from "@microsoft/sp-http"; -import { sp } from "@pnp/sp/presets/all"; +import { IInstalledLanguageInfo, sp } from "@pnp/sp/presets/all"; import * as strings from "ControlStrings"; import { DefaultButton, @@ -207,8 +207,12 @@ export class DynamicForm extends React.Component< val.fieldDefaultValue = null; shouldBeReturnBack = true; } - } else if (val.fieldType === "Number") { - if ((val.newValue < val.minimumValue) || (val.newValue > val.maximumValue)) { + } + if (val.fieldType === "Number") { + if (this.isEmptyNumOrString(val.newValue) && (val.minimumValue != null || val.maximumValue != null)) { + val.newValue = val.fieldDefaultValue = null; + } + if (!this.isEmptyNumOrString(val.newValue) && (isNaN(Number(val.newValue)) || (val.newValue < val.minimumValue) || (val.newValue > val.maximumValue))) { shouldBeReturnBack = true; } } @@ -514,6 +518,7 @@ export class DynamicForm extends React.Component< order++; const fieldType = field.TypeAsString; field.order = order; + let cultureName: string; let hiddenName = ""; let termSetId = ""; let anchorId = ""; @@ -539,10 +544,14 @@ export class DynamicForm extends React.Component< }); } else if (fieldType === "Note") { richText = field.RichText; - } else if (fieldType === "Number") { + } else if (fieldType === "Number" || fieldType === "Currency") { minValue = field.MinimumValue; maxValue = field.MaximumValue; - showAsPercentage = field.ShowAsPercentage; + if (fieldType === "Number") { + showAsPercentage = field.ShowAsPercentage; + } else { + cultureName = this.cultureNameLookup(field.CurrencyLocaleId); + } } else if (fieldType === "Lookup") { lookupListId = field.LookupList; lookupField = field.LookupField; @@ -689,8 +698,8 @@ export class DynamicForm extends React.Component< defaultValue = JSON.parse(defaultValue); } else if (fieldType === "Boolean") { defaultValue = Boolean(Number(defaultValue)); - } - + } + tempFields.push({ newValue: null, fieldTermSetId: termSetId, @@ -698,6 +707,7 @@ export class DynamicForm extends React.Component< options: choices, lookupListID: lookupListId, lookupField: lookupField, + cultureName, changedValue: defaultValue, fieldType: field.TypeAsString, fieldTitle: field.Title, @@ -722,13 +732,18 @@ export class DynamicForm extends React.Component< description: field.Description, minimumValue: minValue, maximumValue: maxValue, - showAsPercentage: showAsPercentage, + showAsPercentage: showAsPercentage }); tempFields.sort((a, b) => a.Order - b.Order); } } - this.setState({ fieldCollection: tempFields, etag: etag }); + let installedLanguages: IInstalledLanguageInfo[]; + if (tempFields.filter(f => f.fieldType === "Currency").length > 0) { + installedLanguages = await sp.web.regionalSettings.getInstalledLanguages(); + } + + this.setState({ fieldCollection: tempFields, installedLanguages, etag }); //return arrayItems; } catch (error) { console.log(`Error get field informations`, error); @@ -736,6 +751,12 @@ export class DynamicForm extends React.Component< } }; + private cultureNameLookup(lcid: number): string { + const pageCulture = this.props.context.pageContext.cultureInfo.currentCultureName; + if (!lcid) return pageCulture; + return this.state.installedLanguages?.find(lang => lang.Lcid === lcid).DisplayName ?? pageCulture; + } + private uploadImage = async ( file: IFilePickerResult ): Promise => { @@ -840,4 +861,9 @@ export class DynamicForm extends React.Component< return errorMessage; }; + + private isEmptyNumOrString(value: string | number) { + if (value == null) return true; + if ((value?.toString().trim().length || 0) === 0) return true; + } } diff --git a/src/controls/dynamicForm/IDynamicFormState.ts b/src/controls/dynamicForm/IDynamicFormState.ts index b182c3caa..2f3de2198 100644 --- a/src/controls/dynamicForm/IDynamicFormState.ts +++ b/src/controls/dynamicForm/IDynamicFormState.ts @@ -1,7 +1,9 @@ +import { IInstalledLanguageInfo } from '@pnp/sp/regional-settings'; import { IDynamicFieldProps } from './dynamicField/IDynamicFieldProps'; export interface IDynamicFormState { fieldCollection: IDynamicFieldProps[]; + installedLanguages?: IInstalledLanguageInfo[]; isSaving?: boolean; etag?: string; isValidationErrorDialogOpen: boolean; diff --git a/src/controls/dynamicForm/dynamicField/DynamicField.tsx b/src/controls/dynamicForm/dynamicField/DynamicField.tsx index 7e0d62939..b37c70cef 100644 --- a/src/controls/dynamicForm/dynamicField/DynamicField.tsx +++ b/src/controls/dynamicForm/dynamicField/DynamicField.tsx @@ -22,6 +22,7 @@ import { IPickerTerms, TaxonomyPicker } from '../../taxonomyPicker'; import styles from '../DynamicForm.module.scss'; import { IDynamicFieldProps } from './IDynamicFieldProps'; import { IDynamicFieldState } from './IDynamicFieldState'; +import CurrencyMap from "../CurrencyMap"; export class DynamicField extends React.Component { @@ -76,7 +77,9 @@ export class DynamicField extends React.Component; - case 'Number': - //eslint-disable-next-line no-case-declarations + case 'Number': { const customNumberErrorMessage = this.getNumberErrorText(); return
@@ -276,11 +278,15 @@ export class DynamicField extends React.Component { this.onChange(newText); }} disabled={disabled} onBlur={this.onBlur} - errorMessage={customNumberErrorMessage} /> + errorMessage={customNumberErrorMessage} + min={minimumValue} + max={maximumValue} /> {descriptionEl}
; + } + case 'Currency': { + const customNumberErrorMessage = this.getNumberErrorText(); - case 'Currency': return
@@ -294,10 +300,12 @@ export class DynamicField extends React.Component { this.onChange(newText); }} disabled={disabled} onBlur={this.onBlur} - errorMessage={errorText} /> + errorMessage={customNumberErrorMessage} + min={minimumValue} + max={maximumValue} /> {descriptionEl}
; - + } case 'DateTime': return
@@ -600,6 +608,8 @@ export class DynamicField extends React.Component 0) { - if (minValue !== undefined && maxValue !== undefined && (changedValue < minValue || changedValue > maxValue)) { - return strings.DynamicFormNumberValueMustBeBetween.replace('{0}', minValue.toString()).replace('{1}', maxValue.toString()); + const numericValue = Number(changedValue); + if (isNaN(numericValue)) return strings.ProvidedValueIsInvalid; + if (minValue !== undefined && maxValue !== undefined && (numericValue < minValue || numericValue > maxValue)) { + return strings.DynamicFormNumberValueMustBeBetween.replace('{0}', minValueCur ?? minValue.toString()).replace('{1}', maxValueCur ?? maxValue.toString()); } else { - if (minValue !== undefined && changedValue < minValue) { - return strings.DynamicFormNumberValueMustBeGreaterThan.replace('{0}', minValue.toString()); + if (minValue !== undefined && numericValue < minValue) { + return strings.DynamicFormNumberValueMustBeGreaterThan.replace('{0}', minValueCur ?? minValue.toString()); } - else if (maxValue !== undefined && changedValue > maxValue) { - return strings.DynamicFormNumberValueMustBeLowerThan.replace('{0}', maxValue.toString()); + else if (maxValue !== undefined && numericValue > maxValue) { + return strings.DynamicFormNumberValueMustBeLowerThan.replace('{0}', maxValueCur ?? maxValue.toString()); } } } diff --git a/src/controls/dynamicForm/dynamicField/IDynamicFieldProps.ts b/src/controls/dynamicForm/dynamicField/IDynamicFieldProps.ts index f1d2b0f09..8a5b425c0 100644 --- a/src/controls/dynamicForm/dynamicField/IDynamicFieldProps.ts +++ b/src/controls/dynamicForm/dynamicField/IDynamicFieldProps.ts @@ -11,6 +11,7 @@ export interface IDynamicFieldProps { listId: string; listItemId?: number; columnInternalName: string; + cultureName?: string; label?: string; placeholder?: string; onChanged?: (columnInternalName: string, newValue: any, additionalData?: FieldChangeAdditionalData) => void; // eslint-disable-line @typescript-eslint/no-explicit-any From 83fe2e8d77d6e8837b2607713995e01647bbe9ca Mon Sep 17 00:00:00 2001 From: Thomas German Date: Mon, 31 Jul 2023 02:28:46 +0100 Subject: [PATCH 2/3] Fix for percentage saving incorrectly --- src/controls/dynamicForm/DynamicForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controls/dynamicForm/DynamicForm.tsx b/src/controls/dynamicForm/DynamicForm.tsx index 5ad9ddfbe..3746e821f 100644 --- a/src/controls/dynamicForm/DynamicForm.tsx +++ b/src/controls/dynamicForm/DynamicForm.tsx @@ -209,6 +209,7 @@ export class DynamicForm extends React.Component< } } if (val.fieldType === "Number") { + if (val.showAsPercentage) val.newValue /= 100; if (this.isEmptyNumOrString(val.newValue) && (val.minimumValue != null || val.maximumValue != null)) { val.newValue = val.fieldDefaultValue = null; } From 3daff4e90e44a0b3be6bd95c2c621b560d314b55 Mon Sep 17 00:00:00 2001 From: Tom German Date: Fri, 6 Oct 2023 13:14:14 +0100 Subject: [PATCH 3/3] Fixing some warnings/clarity, and enum import issue on ControlsTest webpart. --- src/controls/dynamicForm/DynamicForm.tsx | 8 ++++---- .../collectionDataItem/CollectionDataItem.tsx | 8 ++++++-- src/webparts/controlsTest/components/ControlsTest.tsx | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/controls/dynamicForm/DynamicForm.tsx b/src/controls/dynamicForm/DynamicForm.tsx index c94f431c3..1a79bc63a 100644 --- a/src/controls/dynamicForm/DynamicForm.tsx +++ b/src/controls/dynamicForm/DynamicForm.tsx @@ -210,7 +210,7 @@ export class DynamicForm extends React.Component< } if (val.fieldType === "Number") { if (val.showAsPercentage) val.newValue /= 100; - if (this.isEmptyNumOrString(val.newValue) && (val.minimumValue != null || val.maximumValue != null)) { + if (this.isEmptyNumOrString(val.newValue) && (val.minimumValue !== null || val.maximumValue !== null)) { val.newValue = val.fieldDefaultValue = null; } if (!this.isEmptyNumOrString(val.newValue) && (isNaN(Number(val.newValue)) || (val.newValue < val.minimumValue) || (val.newValue > val.maximumValue))) { @@ -345,7 +345,7 @@ export class DynamicForm extends React.Component< try { const contentTypeIdField = "ContentTypeId"; //check if item contenttype is passed, then update the object with content type id, else, pass the object - contentTypeId !== undefined && contentTypeId.startsWith("0x01") ? objects[contentTypeIdField] = contentTypeId : objects; + if (contentTypeId !== undefined && contentTypeId.startsWith("0x01")) objects[contentTypeIdField] = contentTypeId; const iar = await sp.web.lists.getById(listId).items.add(objects); if (onSubmitted) { onSubmitted( @@ -871,8 +871,8 @@ export class DynamicForm extends React.Component< return errorMessage; }; - private isEmptyNumOrString(value: string | number) { - if (value == null) return true; + private isEmptyNumOrString(value: string | number): boolean { + if (value === null) return true; if ((value?.toString().trim().length || 0) === 0) return true; } } diff --git a/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx b/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx index 78f72a3ba..f686c47ba 100644 --- a/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx +++ b/src/controls/fieldCollectionData/collectionDataItem/CollectionDataItem.tsx @@ -366,7 +366,7 @@ export class CollectionDataItem extends React.Component { let isValid = true; - let validation = ""; + const validation = ""; if (field.required && (selected === null || selected.length === 0) ) { isValid = false; @@ -567,7 +567,11 @@ export class CollectionDataItem extends React.Component{ - field.multiSelect ? this.onValueChangedComboBoxMulti(field.id, option, value) : this.onValueChangedComboBoxSingle(field.id, option, value) + if (field.multiSelect) { + this.onValueChangedComboBoxMulti(field.id, option, value); + } else { + this.onValueChangedComboBoxSingle(field.id, option, value); + } } } />; diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 356611fcb..4c5a0d135 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -146,7 +146,7 @@ import { import { PeoplePicker, PrincipalType -} from "../../../PeoplePicker"; +} from "../../../controls/peoplepicker"; import { Placeholder } from "../../../Placeholder"; import { IProgressAction,