/////////////////////////////// // gzform // // provides form services and utilities // validation services // dirty and change tracking // and also general error display in forms //probably should be broken up more // All translation keys for validation *MUST* be fetched prior to this being used as it assumes all keys are fetched first // Add any new keys used to the block in translation.js=>commonKeysEditForm import Vue from "vue"; let triggeringChange = false; function isEmpty(o) { if (typeof o == "number" && o == 0) { return false; } return !o; } //////////////////////////////////// // isInt value?? // //FROM HERE: https://stackoverflow.com/a/14794066/8939 //fast test if is an integer: function isInt(value) { let x; if (isNaN(value)) { return false; } x = parseFloat(value); return (x | 0) === x; } //////////////////////////////////// // isNumber // //FROM HERE: https://stackoverflow.com/a/1830632/8939 function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); } //////////////////////////////////// // Get control from ref // function getControl(vm, ref) { return vm.$refs[ref]; } //////////////////////////////////// // Get value from control // function getControlValue(ctrl) { return ctrl.value; } //////////////////////////////////// // Get field name from control // function getControlLabel(ctrl) { if (ctrl.label == undefined) { return "UNKNOWN CONTROL"; } else { return ctrl.label; } } ///////////////////////////////////////// // Get errors for a particular field // from server error collection // function getErrorsForField(vm, ref) { //Note: to debug this on forms just put {{ formState.serverError }} //on the form to see what is actually stored there and should be showing let ret = []; if (ref == "generalerror") { ret = vm.formState.serverError.details.filter( z => z.target == false || z.target == "generalerror" ); } else { ret = vm.formState.serverError.details.filter(function(o) { if (!o.target) { return false; } //server error fields are capitalized //client field names are generally lower case except for custom fields //so we need to normalize them all to lower case to match //they will always differ by more than case so this is fine //NOTE: Indexed child collection error target field names are in this scheme "Items[2].FieldName" //where Items is the name of the parent models property that contains the child collection return o.target.toLowerCase() == ref.toLowerCase(); }); } return ret; } /////////////////////////////// // ERROR BOX ERRORS // gathers any messages for error box on form which is the generic catch all for non field specific errors from server // and application itself locally function getErrorBoxErrors(vm, errs) { let hasErrors = false; let ret = ""; if (errs.length > 0) { hasErrors = true; //loop array and append each error to a return string for (let i = 0; i < errs.length; i++) { ret += errs[i] + "\r\n"; } } //any application errors? if (vm.formState.appError) { hasErrors = true; // console.log("gzform:geterrorboxerrors ", { // appError: vm.formState.appError, // ret: ret // }); ret += vm.formState.appError; } if (!hasErrors) { return null; } else { return ret; } } export default { /////////////////////////////// // REQUIRED // required(vm, ref) { if (vm.formState.loading) { return true; } const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (!isEmpty(value)) { return true; } // "ErrorRequiredFieldEmpty": "{0} is a required field. Please enter a value for {0}", let err = vm.$ay.t("ErrorRequiredFieldEmpty"); const fieldName = getControlLabel(ctrl); err = err.replace("{0}", fieldName); //replace only replaces first instance so need to do it twice err = err.replace("{0}", fieldName); this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // MAXLENGTH // maxLength(vm, ref, max) { if (vm.formState.loading) { return true; } const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (isEmpty(value)) { return true; } if (value.length > max) { //get the translated rule text // "ErrorFieldLengthExceeded": "{0} can not exceed {1} characters.", let err = vm.$ay.t("ErrorFieldLengthExceeded"); const fieldName = getControlLabel(ctrl); err = err.replace("{0}", fieldName); err = err.replace("{1}", max); this.setFormState({ vm: vm, valid: false }); return err; } else { return true; } }, /////////////////////////////// // MAX 255 // max255(vm, ref) { if (vm.formState.loading) { return true; } return this.maxLength(vm, ref, 255); }, /////////////////////////////// // DatePrecedence // (start date must precede end date, however if both are empty then that's ok) // datePrecedence(vm, refStart, refEnd) { if (vm.formState.loading) { return true; } const ctrlStart = getControl(vm, refStart); if (typeof ctrlStart == "undefined") { return true; } const ctrlEnd = getControl(vm, refEnd); if (typeof ctrlEnd == "undefined") { return true; } let valueStart = getControlValue(ctrlStart); if (isEmpty(valueStart)) { return true; } let valueEnd = getControlValue(ctrlEnd); if (isEmpty(valueEnd)) { return true; } valueStart = window.$gz.DateTime.fromISO(valueStart); valueEnd = window.$gz.DateTime.fromISO(valueEnd); // if either is not valid. //moment.github.io/luxon/docs/manual/validity.html if (!valueStart.isValid || !valueEnd.isValid) { return true; } if (valueStart > valueEnd) { // "ErrorStartDateAfterEndDate": "Start date must be earlier than stop / end date", const err = vm.$ay.t("ErrorStartDateAfterEndDate"); this.setFormState({ vm: vm, valid: false }); return err; } else { return true; } }, /////////////////////////////// // Confirm password // (two fields must match) // confirmMatch(vm, refFirst, refSecond) { if (vm.formState.loading) { return true; } const ctrlFirst = getControl(vm, refFirst); if (typeof ctrlFirst == "undefined") { return true; } const ctrlSecond = getControl(vm, refSecond); if (typeof ctrlSecond == "undefined") { return true; } const valueFirst = getControlValue(ctrlFirst); const valueSecond = getControlValue(ctrlSecond); if (valueFirst != valueSecond) { const err = vm.$ay.t("ErrorNoMatch"); this.setFormState({ vm: vm, valid: false }); return err; } else { return true; } }, /////////////////////////////// // INTEGER IS VALID // integerValid(vm, ref) { if (vm.formState.loading) { return true; } const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (isInt(value)) { return true; } const err = vm.$ay.t("ErrorFieldValueNotInteger"); this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // DECIMAL // Basically anything that can be a number is valid // decimalValid(vm, ref) { if (vm.formState.loading) { return true; } //TODO: Handle commas and spaces in numbers //as per window.$gz.translation rules for numbers const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (isEmpty(value)) { return true; } if (isNumber(value)) { return true; } const err = vm.$ay.t("ErrorFieldValueNotDecimal"); this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // MAX VALUE // Maximum numeric value // maxValue is lt or eq // empty is considered valid for this rule // maxValueValid(vm, ref, maxValue) { if (vm.formState.loading) { return true; } const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (isEmpty(value)) { return true; } if (!isNumber(value)) { return true; } //Ok, were here with a non empty number of some kind if (value <= maxValue) { return true; } const err = `${vm.$ay .t("ErrorFieldValueNumberGreaterThanMax") .replace("{0}", maxValue)} ${maxValue}`; this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // MIN VALUE // Minimum numeric value // minValue is gt or eq // empty is considered valid for this rule // minValueValid(vm, ref, minValue) { if (vm.formState.loading) { return true; } const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (isEmpty(value)) { return true; } if (!isNumber(value)) { return true; } //Ok, were here with a non empty number of some kind //actual check if (value >= minValue) { return true; } const err = `${vm.$ay .t("ErrorFieldValueNumberLessThanMin") .replace("{0}", minValue)} ${minValue}`; this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // EMAIL IS VALID-ish //https://tylermcginnis.com/validate-email-address-javascript/ emailValid(vm, ref) { if (vm.formState.loading) { return true; } const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (isEmpty(value)) { return true; } if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) == true) { return true; } const err = vm.$ay.t("ErrorAPI2203"); //"Invalid value" this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // USER REQUIRED FIELDS // (Fields defined by AyaNova users as required on form that are not stock required already) userRequiredFields(vm, ref, formCustomTemplateFieldName) { if (vm.formState.loading) { return true; } const template = window.$gz.store.state.formCustomTemplate[vm.formCustomTemplateKey]; if (template === undefined) { return true; } //See if control formCustomTemplateFieldName is in server required fields collection //this is a collection of both custom field definitions and standard form fields that are required //since all names are unique can just filter out the one we need by name which will inherently ignore custom fields by default const templateItem = template.find( z => z.fld == formCustomTemplateFieldName ); //templateItem.required if (templateItem === undefined || templateItem.required !== true) { return true; } const ctrl = getControl(vm, ref); if (typeof ctrl == "undefined") { return true; } const value = getControlValue(ctrl); if (!isEmpty(value)) { return true; } let err = vm.$ay.t("ErrorRequiredFieldEmpty"); const fieldName = getControlLabel(ctrl); err = err.replace("{0}", fieldName); //replace only replaces first instance so need to do it twice err = err.replace("{0}", fieldName); this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // CUSTOMFIELDS // For now the only rule is that they can be required or not // customFieldsCheck(vm, templateItem, subvm, fieldName) { if (vm.formState.loading) { return true; } if (templateItem.required !== true) { return true; } const value = subvm.GetValueForField(templateItem.dataKey); if (!isEmpty(value)) { return true; } //It's empty and it's required so return error let err = vm.$ay.t("ErrorRequiredFieldEmpty"); err = err.replace("{0}", fieldName); //replace only replaces first instance so need to do it twice err = err.replace("{0}", fieldName); this.setFormState({ vm: vm, valid: false }); return err; }, /////////////////////////////// // SERVER ERRORS // Process and return server errors if any for form and field specified // note that this is called in turn by every control on the form so it's only job // is to return errors if they exist for *that* field // Not to be confused with validation errors through the "rules" property // that is separate and any errors returned here are *added* to the validation // errors in the UI by Vuetify // serverErrors(vm, ref) { const ret = []; //check for errors if we have any errors if (!window.$gz.util.objectIsEmpty(vm.formState.serverError)) { //First let's get the top level error code const apiErrorCode = parseInt(vm.formState.serverError.code); //Not all server errors mean the form is invalid, exceptions here let formValid = false; /* These errors are not the user's fault and no changes to the form are required so they may be temporary and user should be able to retry save API_CLOSED = 2000, API_OPS_ONLY = 2001, API_SERVER_ERROR = 2002, VALIDATION_REFERENTIAL_INTEGRITY = 2208 */ switch (apiErrorCode) { case 2000: case 2001: case 2002: case 2208: formValid = true; //we came here because the user saved because the form was valid so it's safe to set that the same again break; default: formValid = false; } //GENERAL ERROR if (ref == "generalerror") { //Add any general errors to ret (specific detail errors for the "generalerror" will be processed later below) let err = vm.$ay.t("ErrorAPI" + apiErrorCode.toString()); if (vm.formState.serverError.message) { err = err + "\r\n" + vm.formState.serverError.message; } this.setFormState({ vm: vm, valid: formValid }); ret.push(err); } //DETAIL ERRORS //{"error":{"code":"2200","details":[{"message":"Exception: Error converting value \"\" to type 'AyaNova.Biz.AUTHORIZATION_ROLES'. Path 'roles', line 1, position 141.","target":"roles","error":"2203"}],"message":"Object did not pass validation"}} //Specific field validation errors are in an array in "details" key if (!window.$gz.util.objectIsEmpty(vm.formState.serverError.details)) { //See if this key is in the details array const errorsForField = getErrorsForField(vm, ref); if (errorsForField.length > 0) { //iterate the errorsForField object and add each to return array of errors //de-lodash //window.$gz. _.each(errorsForField, function(ve) { errorsForField.forEach(function(ve) { let fldErr = ""; const fldErrorCode = parseInt(ve.error); fldErr = vm.$ay.t("ErrorAPI" + fldErrorCode.toString()) + " [" + ve.error + "]"; if (ve.message) { //NOTE: call sync version here as can't call async code from here //so translations must already be pre-fetched to work here fldErr += ' - "' + window.$gz.translation.translateStringWithMultipleKeys( ve.message ) + '"'; ret.push(fldErr); } else { ret.push(fldErr); } }); this.setFormState({ vm: vm, valid: false }); return ret; } } } //default if no error message to display return ret; }, /////////////////////////////// // childRowHasError // returns true if error exists for row // else returns false // (actual errors not returned just for row indicator, // user opens child edit form to see exact error) // childRowHasError(vm, path) { //Note: this just shows server errors, not local form validation errors //it's assumed user will fix in form or when they submit see the error come back //Note: this method is easily converted to return actual errors if it ever makes sense to do that but for now I'm ok with row TTM //No server errors? if (window.$gz.util.objectIsEmpty(vm.formState.serverError)) { //nothing to process return null; } //no detail errors? if (window.$gz.util.objectIsEmpty(vm.formState.serverError.details)) { //nothing to process return null; } path = path.toLowerCase(); //Might be an error, check if collectionName is in error collection //this is what we're dealing with // { "code": "2200", "details": [ { "message": "LT:PurchaseOrderReceiptItemQuantityReceivedErrorInvalid", "target": "Items[0].QuantityReceived", "error": "2203" } ], "message": "ErrorAPI2200" } //or multilayered like this: target: "Items[3].scheduledUsers[1].EstimatedQuantity" //so set "path" accordingly easy peasy, such error, so wow //filter in items that start with the row collection name and index provided return vm.formState.serverError.details.some(function(o) { if (!o.target) { return false; } return o.target.toLowerCase().includes(path); }); }, /////////////////////////////// // childRowErrorClass // returns class to set on row if error exists for row // else returns nothing //( called by some forms but notably not the workorder or PO which have this built in) // childRowErrorClass(vm, collectionName, rowIndex) { if (this.childRowHasError(vm, collectionName, rowIndex)) { return "font-weight-black font-italic error--text"; } return null; }, /////////////////////////////// // ShowMe // (returns false if the field has been set to hidden by the user in the formcustomtemplate) // NOTE: that in a form this should only be used with non stock-required fields, if they are already required they cannot be hidden // showMe(vm, formCustomTemplateFieldName) { //special check for wiki field //if read only then can't add any content and if no content then no reason to show it at all if (formCustomTemplateFieldName == "wiki") { if ( vm.formState.readOnly && (vm.obj.wiki == null || vm.obj.wiki.length == 0) ) { return false; } } const template = window.$gz.store.state.formCustomTemplate[vm.formCustomTemplateKey]; if (template === undefined) { return true; } //See if control templateFieldName is in server required fields collection //this is a collection of both custom field definitions and standard form fields that are required //since all names are unique can just filter out the one we need by name which will inherently ignore custom fields by default const templateItem = template.find( z => z.fld.toLowerCase() == formCustomTemplateFieldName.toLowerCase() ); if (templateItem === undefined || templateItem.hide !== true) { return true; } //Only here if we have a record in the custom template for this particular field and it's set to hide:true return false; }, /////////////////////////////// // ClearformState.serverErrors // Clear all server errors and app errors and ensure error box doesn't show // deleteAllErrorBoxErrors(vm) { //clear all keys from server error window.$gz.util.removeAllPropertiesFromObject(vm.formState.serverError); //clear app errors vm.formState.appError = null; //clear out actual message box display vm.formState.errorBoxMessage = null; this.setFormState({ vm: vm, valid: true }); }, /////////////////////////////// // setErrorBoxErrors // Gather server errors and set the appropriate keys // setErrorBoxErrors(vm) { const errs = this.serverErrors(vm, "generalerror"); const ret = getErrorBoxErrors(vm, errs); vm.formState.errorBoxMessage = ret; }, /////////////////////////////// // On fieldValueChanged handler // formReference is an optional string name of the form ref property if alternative named form // - Clear server errrors for this field // - Flag form dirty // - check and flag form validity // // fieldValueChanged(vm, ref, formReference) { const that = this; let formControl = null; if (formReference == undefined) { formControl = vm.$refs.form; } else { //NOTE: Due to automatic code formatting some refs may come here with newlines in them resulting in no matches formReference = formReference.replace(/\s/g, ""); formControl = vm.$refs[formReference]; } //dev error on form? if (formControl == null) { if (vm.$ay.dev) { //not necessarily an error, can happen during form init console.trace( `gzform::fieldValueChanged formControl is not found ref:${ref}, formReferences:${formReference} ` ); } else { return; } } //this is currently required to ensure that this method runs after all the broken rule checks have settled Vue.nextTick(function() { //------------- if (triggeringChange || vm.formState.loading) { return; } //# REMOVE SERVER ERRORS FOR THIS FIELD REF const targetRef = ref.toLowerCase(); //NOTE: This block of code is meant to remove all detailed server errors where the Target matches the current referenced control //Then it checks to see if there is anything left in details as this might have been all there was and if so removes that whole thing //leaving only errors for other fields or nothing if this ref field was all the errors left //Remove any server errors that are for our target ref field //and also set a flag if there *are* any server errors for our target field let targetFieldHasServerError = false; if (vm.formState.serverError.details) { let i = vm.formState.serverError.details.length; //iterate backwards so we can mutate the array in place while (i--) { var o = vm.formState.serverError.details[i]; if (o.target && o.target.toLowerCase() == targetRef) { //remove it, it's for our ref field vm.formState.serverError.details.splice(i, 1); targetFieldHasServerError = true; } } } //# CLEAN UP SERVER ERRORS IF NONE LEFT //If there are no more errors in details then remove the whole thing as it's no longer required if ( vm.formState.serverError.details && vm.formState.serverError.details.length < 1 ) { if (vm.formState.serverError.code == "2200") { //clear all keys from server error window.$gz.util.removeAllPropertiesFromObject( vm.formState.serverError ); } } //# CLEAR OUT STALE VALIDATION ERRORS FOR CONTROL //Clear out old validation display in form by forcing the control's data to change //I tried calling form validate and reset but it did nothing //probably because it has safeguards to prevent excess validation, this works though so far //I added the triggering change guard but it actually doesn't seem to be required here, more investigation is required //TODO: find a cleaner way to remove old validation on the control, why can't it just be set on the control referenced?? if (targetFieldHasServerError) { triggeringChange = true; const val = vm.obj[ref]; vm.obj[ref] = null; vm.obj[ref] = val; triggeringChange = false; } //# UPDATE FORM STATUS let formValid = formControl.validate(); that.setFormState({ vm: vm, dirty: true, valid: formValid }); //--------------- }); //next tick end }, setFormState(newState) { //this returns a promise so any function that needs to wait for this can utilize that return Vue.nextTick(function() { if (newState.valid != null) { newState.vm.formState.valid = newState.valid; } if (newState.dirty != null) { newState.vm.formState.dirty = newState.dirty; } if (newState.loading != null) { newState.vm.formState.loading = newState.loading; } if (newState.readOnly != null) { newState.vm.formState.readOnly = newState.readOnly; } if (newState.ready != null) { newState.vm.formState.ready = newState.ready; } }); }, //////////////////////////////////// // Get form settings // for form specified or empty object if there is none // EAch form is responsible for what it stores and how it initializes, this just provides that // the form does the actual work of what settings it requires // Form settings are temp and saved, saved ones go into vuex and localstorage and persist a refresh // and temporary ones are stored in session storage and don't persist a refresh // getFormSettings(formKey) { return { temp: JSON.parse(sessionStorage.getItem(formKey)), saved: window.$gz.store.state.formSettings[formKey] }; }, //////////////////////////////////// // Set form settings // for form key specified // requires object with one or both keys {temp:{...tempformsettings...},saved:{...persistedformsettings...}} // setFormSettings(formKey, formSettings) { if (formSettings.saved) { window.$gz.store.commit("setFormSettings", { formKey: formKey, formSettings: formSettings.saved }); } if (formSettings.temp) { sessionStorage.setItem(formKey, JSON.stringify(formSettings.temp)); } }, //////////////////////////////////// // Get last report used from form settings // getLastReport(formKey) { const fs = window.$gz.store.state.formSettings[formKey]; if (fs == null || fs.lastReport == null) { return null; } return fs.lastReport; }, //////////////////////////////////// // Set last report used in form settings // setLastReportMenuItem(formKey, reportSelected, vm) { let fs = window.$gz.store.state.formSettings[formKey]; if (fs == null) { fs = {}; } fs.lastReport = reportSelected; window.$gz.store.commit("setFormSettings", { formKey: formKey, formSettings: fs }); window.$gz.eventBus.$emit("menu-upsert-last-report", { title: reportSelected.name, notrans: true, icon: "$ayiFileAlt", key: formKey + ":report:" + reportSelected.id, vm: vm }); }, //////////////////////////////////// // Add no selection item // Used by forms that need the option of an unselected // item in a pick list // addNoSelectionItem(listArray, nullNotZero) { if (listArray == undefined || listArray == null) { listArray = []; } listArray.unshift({ name: "-", id: nullNotZero ? null : 0 }); }, //////////////////////////////////// // Get no selection item // Used by forms that need just the // unselected item itself not added // to a list // getNoSelectionItem(nullNotZero) { return { name: "-", id: nullNotZero ? null : 0 }; }, //////////////////////////////////// // Get validity of referenced control // controlIsValid(vm, ref) { if (vm.$refs[ref]) { return vm.$refs[ref].valid; } return false; }, //////////////////////////////////////// // All controls are valid? // controlsAreAllValid(vm, refs) { //if any are not valid return false for (let i = 0; i < refs.length; i++) { const item = refs[i]; if (vm.$refs[item]) { if (!vm.$refs[item].valid) { return false; } } } return true; }, //////////////////////////////////////// // Standard object not found handler // handleObjectNotFound(vm) { window.$gz.eventBus.$emit("notify-error", vm.$ay.t("ErrorAPI2010")); //after small delay to show error //(the navigate removes the toast notification immediately) setTimeout(function() { vm.$router.go(-1); }, 2000); }, //////////////////////////////////////// // Standard data table row error class // tableRowErrorClass() { return "font-weight-black font-italic error--text "; }, //////////////////////////////////////// // Standard data table deleted class // tableRowDeletedClass() { return "text-decoration-line-through "; } };