();
Object.keys(formFields).forEach((otherKey) =>
{
formFields[i].possibleValueProps.otherValues.set(otherKey, values[otherKey]);
});
}
}
if (!Object.keys(formFields).length)
{
return Error: No form fields in section {section.name}
;
}
const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
if(omitWrapper)
{
return
}
return
{section.label}
{getSectionHelp(section)}
}
/*******************************************************************************
** render a section as a widget
*******************************************************************************/
function getWidgetSection(widgetMetaData: QWidgetMetaData, widgetData: any): JSX.Element
{
widgetData.viewAllLink = null;
widgetMetaData.showExportButton = false;
return openAddChildRecord(widgetMetaData.name, widgetData)}
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData, rowIndex)}
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, widgetData, rowIndex)}
/>
}
/*******************************************************************************
** render a form section
*******************************************************************************/
function renderSection(section: QTableSection, values: FormikValues | Value, touched: FormikTouched | Value, formFields: Map, errors: FormikErrors | Value)
{
if(section.fieldNames && section.fieldNames.length > 0)
{
return getFormSection(section, values, touched, formFields.get(section.name), errors);
}
else
{
return renderedWidgetSections[section.widgetName] ?? Loading {section.label}...
}
}
if (!asyncLoadInited)
{
setAsyncLoadInited(true);
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
const metaData = await qController.loadMetaData();
setMetaData(metaData);
/////////////////////////////////////////////////
// define the sections, e.g., for the left-bar //
/////////////////////////////////////////////////
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
{
return section.widgetName && metaData.widgets.get(section.widgetName)?.type == "childRecordList" && metaData.widgets.get(section.widgetName)?.defaultValues?.has("manageAssociationName")
});
setTableSections(tableSections);
const fieldArray = [] as QFieldMetaData[];
const sortedKeys = [...tableMetaData.fields.keys()].sort();
sortedKeys.forEach((key) =>
{
const fieldMetaData = tableMetaData.fields.get(key);
fieldArray.push(fieldMetaData);
});
/////////////////////////////////////////////////////////////////////////////////////////
// if doing an edit or copy, fetch the record and pre-populate the form values from it //
/////////////////////////////////////////////////////////////////////////////////////////
let record: QRecord = null;
let defaultDisplayValues = new Map();
if (props.id !== null)
{
record = await qController.get(tableName, props.id);
setRecord(record);
const titleVerb = props.isCopy ? "Copy" : "Edit";
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
if (!props.isModal)
{
setPageHeader(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
}
tableMetaData.fields.forEach((fieldMetaData, key) =>
{
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
{
return;
}
initialValues[key] = record.values.get(key);
});
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(! props.isCopy)
{
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{
setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
}
}
}
else
{
///////////////////////////////////////////
// else handle preparing to do an insert //
///////////////////////////////////////////
setFormTitle(`Creating New ${tableMetaData?.label}`);
if (!props.isModal)
{
setPageHeader(`Creating New ${tableMetaData?.label}`);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// if default values were supplied for a new record, then populate initialValues, for formik. //
////////////////////////////////////////////////////////////////////////////////////////////////
for (let i = 0; i < fieldArray.length; i++)
{
const fieldMetaData = fieldArray[i];
const fieldName = fieldMetaData.name;
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
if (defaultValue)
{
initialValues[fieldName] = defaultValue;
///////////////////////////////////////////////////////////////////////////////////////////
// we need to set the initialDisplayValue for possible value fields with a default value //
// so, look them up here now if needed //
///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName)
{
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
if (results && results.length > 0)
{
defaultDisplayValues.set(fieldName, results[0].label);
}
}
}
}
}
///////////////////////////////////////////////////
// if an override heading was passed in, use it. //
///////////////////////////////////////////////////
if(props.overrideHeading)
{
setFormTitle(props.overrideHeading);
if (!props.isModal)
{
setPageHeader(props.overrideHeading);
}
}
//////////////////////////////////////
// check capabilities & permissions //
//////////////////////////////////////
if (props.isCopy || !props.id)
{
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{
setNotAllowedError("Records may not be created in this table");
}
else if (!tableMetaData.insertPermission)
{
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
}
}
else
{
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{
setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
}
}
/////////////////////////////////////////////////////////////////////
// make sure all initialValues are properly formatted for the form //
/////////////////////////////////////////////////////////////////////
for (let i = 0; i < fieldArray.length; i++)
{
const fieldMetaData = fieldArray[i];
if (fieldMetaData.type == QFieldType.DATE_TIME && initialValues[fieldMetaData.name])
{
initialValues[fieldMetaData.name] = ValueUtils.formatDateTimeValueForForm(initialValues[fieldMetaData.name]);
}
}
setInitialValues(initialValues);
/////////////////////////////////////////////////////////
// get formField and formValidation objects for Formik //
/////////////////////////////////////////////////////////
const {
dynamicFormFields,
formValidations,
} = DynamicFormUtils.getFormData(fieldArray, disabledFields);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues);
/////////////////////////////////////
// group the formFields by section //
/////////////////////////////////////
const dynamicFormFieldsBySection = new Map();
let t1sectionName;
let t1section;
const nonT1Sections: QTableSection[] = [];
const newRenderedWidgetSections: {[name: string]: JSX.Element} = {};
const newChildListWidgetData: {[name: string]: ChildRecordListData} = {};
for (let i = 0; i < tableSections.length; i++)
{
const section = tableSections[i];
const sectionDynamicFormFields: any[] = [];
if (section.isHidden)
{
continue;
}
const hasFields = section.fieldNames && section.fieldNames.length > 0;
const hasChildRecordListWidget = section.widgetName && metaData.widgets.get(section.widgetName)?.type == "childRecordList"
if(!hasFields && !hasChildRecordListWidget)
{
continue;
}
if(hasFields)
{
for (let j = 0; j < section.fieldNames.length; j++)
{
const fieldName = section.fieldNames[j];
const field = tableMetaData.fields.get(fieldName);
if (!field)
{
console.log(`Omitting un-found field ${fieldName} from form`);
continue;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if ((props.id !== null && !props.isCopy) || field.isEditable)
{
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
}
}
if (sectionDynamicFormFields.length === 0)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// in case there are no active fields in this section, remove it from the tableSections array //
////////////////////////////////////////////////////////////////////////////////////////////////
tableSections.splice(i, 1);
i--;
continue;
}
else
{
dynamicFormFieldsBySection.set(section.name, sectionDynamicFormFields);
}
}
else
{
const widgetMetaData = metaData.widgets.get(section.widgetName);
const widgetData = await qController.widget(widgetMetaData.name, props.id ? `${tableMetaData.primaryKeyField}=${props.id}` : "");
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData);
newChildListWidgetData[section.widgetName] = widgetData;
}
//////////////////////////////////////
// capture the tier1 section's name //
//////////////////////////////////////
if (section.tier === "T1")
{
t1sectionName = section.name;
t1section = section;
}
else
{
nonT1Sections.push(section);
}
}
setT1SectionName(t1sectionName);
setT1Section(t1section);
setNonT1Sections(nonT1Sections);
setFormFields(dynamicFormFieldsBySection);
setValidations(Yup.object().shape(formValidations));
setRenderedWidgetSections(newRenderedWidgetSections);
setChildListWidgetData(newChildListWidgetData);
forceUpdate();
})();
}
//////////////////////////////////////////////////////////////////
// watch widget data - if they change, re-render those sections //
//////////////////////////////////////////////////////////////////
useEffect(() =>
{
if(childListWidgetData)
{
const newRenderedWidgetSections: {[name: string]: JSX.Element} = {};
for(let name in childListWidgetData)
{
const widgetMetaData = metaData.widgets.get(name);
newRenderedWidgetSections[name] = getWidgetSection(widgetMetaData, childListWidgetData[name]);
}
setRenderedWidgetSections(newRenderedWidgetSections);
}
}, [childListWidgetData]);
const handleCancelClicked = () =>
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - we might have rather just done a navigate(-1) (to keep history clean) //
// but if the user used the anchors on the page, this doesn't effectively cancel... //
// what we have here pushed a new history entry (I think?), so could be better //
///////////////////////////////////////////////////////////////////////////////////////
if (props.id !== null && props.isCopy)
{
const path = `${location.pathname.replace(/\/copy$/, "")}`;
navigate(path, {replace: true});
}
else if (props.id !== null)
{
const path = `${location.pathname.replace(/\/edit$/, "")}`;
navigate(path, {replace: true});
}
else
{
const path = `${location.pathname.replace(/\/create$/, "")}`;
navigate(path, {replace: true});
}
};
/*******************************************************************************
** event handler for the (Formik) Form.
*******************************************************************************/
const handleSubmit = async (values: any, actions: any) =>
{
actions.setSubmitting(true);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there anre return. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(props.onSubmitCallback)
{
props.onSubmitCallback(values);
return;
}
await (async () =>
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we will be manipulating the values sent to the backend, so clone values so they remained unchanged for the form widgets //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const valuesToPost = JSON.parse(JSON.stringify(values));
for(let fieldName of tableMetaData.fields.keys())
{
const fieldMetaData = tableMetaData.fields.get(fieldName);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// (1) convert date-time fields from user's time-zone into UTC //
// (2) if there's an initial value which matches the value (e.g., from the form), then remove that field //
// from the set of values that we'll submit to the backend. This is to deal with the fact that our //
// date-times in the UI (e.g., the form field) only go to the minute - so they kinda always end up //
// changing from, say, 12:15:30 to just 12:15:00... this seems to get around that, for cases when the //
// user didn't change the value in the field (but if the user did change the value, then we will submit it) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(fieldMetaData.type === QFieldType.DATE_TIME && valuesToPost[fieldName])
{
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${valuesToPost[fieldName]}]`)
if (initialValues[fieldName] == valuesToPost[fieldName])
{
console.log(" - Is the same, so, deleting from the post");
delete (valuesToPost[fieldName]);
}
else
{
valuesToPost[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(valuesToPost[fieldName]);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for BLOB fields, there are 3 possible cases: //
// 1) they are a File object - in which case, cool, send them through to the backend to have bytes stored. //
// 2) they are null - in which case, cool, send them through to the backend to be set to null. //
// 3) they are a String, which is their URL path to download them... in that case, don't submit them to //
// the backend at all, so they'll stay what they were. do that by deleting them from the values object here. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(fieldMetaData.type === QFieldType.BLOB)
{
if(typeof valuesToPost[fieldName] === "string")
{
console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`);
delete(valuesToPost[fieldName]);
}
else
{
valuesToPost[fieldName] = values[fieldName];
}
}
}
const associationsToPost: any = {}
let haveAssociationsToPost = false;
for (let name of Object.keys(childListWidgetData))
{
const manageAssociationName = metaData.widgets.get(name)?.defaultValues?.get("manageAssociationName")
if(!manageAssociationName)
{
console.log(`Cannot send association data to backend - missing a manageAssociationName defaultValue in widget meta data for widget name ${name}`);
}
associationsToPost[manageAssociationName] = [];
haveAssociationsToPost = true;
for(let i=0; i
{
if (props.isModal)
{
props.closeModalHandler(null, "recordUpdated");
}
else
{
let warningMessage = null;
if(record.warnings && record.warnings.length && record.warnings.length > 0)
{
warningMessage = record.warnings[0];
}
const path = location.pathname.replace(/\/edit$/, "");
navigate(path, {state: {updateSuccess: true, warning: warningMessage}});
}
})
.catch((error) =>
{
console.log("Caught:");
console.log(error);
if(error.message.toLowerCase().startsWith("warning"))
{
const path = location.pathname.replace(/\/edit$/, "");
navigate(path, {state: {updateSuccess: true, warning: error.message}});
}
else
{
setAlertContent(error.message);
HtmlUtils.autoScroll(0);
}
});
}
else
{
/////////////////////////////////
// perform an insert //
// todo - audit if it's a dupe //
/////////////////////////////////
await qController
.create(tableName, valuesToPost)
.then((record) =>
{
if (props.isModal)
{
props.closeModalHandler(null, "recordCreated");
}
else
{
let warningMessage = null;
if(record.warnings && record.warnings.length && record.warnings.length > 0)
{
warningMessage = record.warnings[0];
}
const path = props.isCopy ?
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
navigate(path, {state: {createSuccess: true, warning: warningMessage}});
}
})
.catch((error) =>
{
if(error.message.toLowerCase().startsWith("warning"))
{
const path = props.isCopy ?
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
navigate(path, {state: {createSuccess: true, warning: error.message}});
}
else
{
setAlertContent(error.message);
HtmlUtils.autoScroll(0);
}
});
}
})();
};
const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
let body;
const getSectionHelp = (section: QTableSection) =>
{
const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
const formattedHelpContent = ;
return formattedHelpContent && (
{formattedHelpContent}
)
}
if (notAllowedError)
{
body = (
{notAllowedError}
{props.isModal &&
}
);
}
else
{
body = (
{
(alertContent || warningContent) &&
{alertContent ? (
setAlertContent(null)}>{alertContent}
) : ("")}
{warningContent ? (
setWarningContent(null)}>{warningContent}
) : ("")}
}
{
!props.isModal &&
}
{({
values,
errors,
touched,
isSubmitting,
}) => (
)}
{
showEditChildForm &&
closeEditChildForm(event, reason)}>
}
);
}
if (props.isModal)
{
return (
{body}
);
}
else
{
return (body);
}
}
function ScrollToFirstError(): JSX.Element
{
const {submitCount, isValid} = useFormikContext()
useEffect(() =>
{
/////////////////////////////////////////////////////////////////////////////
// Wrap the code in setTimeout to make sure it runs after the DOM has been //
// updated and has the error message elements. //
/////////////////////////////////////////////////////////////////////////////
setTimeout(() =>
{
////////////////////////////////////////
// Only run on submit or if not valid //
////////////////////////////////////////
if (submitCount === 0 || isValid)
{
return;
}
//////////////////////////////////
// Find the first error message //
//////////////////////////////////
const errorMessageSelector = "[data-field-error]";
const firstErrorMessage = document.querySelector(errorMessageSelector);
if (!firstErrorMessage)
{
console.warn(`Form failed validation but no error field was found with selector: ${errorMessageSelector}`);
return;
}
firstErrorMessage.scrollIntoView({block: "center"});
}, 100)
}, [submitCount, isValid])
return null;
}
export default EntityForm;