Merge pull request #48 from sartography/feature/add_times_to_instance_filter

Feature/add times to instance filter
This commit is contained in:
jasquat 2022-11-17 16:38:32 -05:00 committed by GitHub
commit 4a9e4c820c
9 changed files with 297 additions and 171 deletions

View File

@ -24,7 +24,6 @@
"@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^5.0.0-beta.13",
"@rjsf/validator-ajv6": "^5.0.0-beta.13",
"@rjsf/validator-ajv8": "^5.0.0-beta.13",
"@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
@ -4950,23 +4949,6 @@
"@rjsf/utils": "^5.0.0-beta.1"
}
},
"node_modules/@rjsf/validator-ajv8": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.13.tgz",
"integrity": "sha512-/hrYbiwgCvEqw1Z7YZTWvd+ZAiX5vSN0WAI2hJTJTqKuCTcIH0fqNDCaOg3FBR38BL7seZrUmibIUcPU66iJ1w==",
"dependencies": {
"ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@rjsf/utils": "^5.0.0-beta.12"
}
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -6840,27 +6822,6 @@
"ajv": "^6.9.1"
}
},
"node_modules/ajv8": {
"name": "ajv",
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
"integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv8/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@ -34883,17 +34844,6 @@
"lodash-es": "^4.17.15"
}
},
"@rjsf/validator-ajv8": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.13.tgz",
"integrity": "sha512-/hrYbiwgCvEqw1Z7YZTWvd+ZAiX5vSN0WAI2hJTJTqKuCTcIH0fqNDCaOg3FBR38BL7seZrUmibIUcPU66iJ1w==",
"requires": {
"ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15"
}
},
"@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -36367,24 +36317,6 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"requires": {}
},
"ajv8": {
"version": "npm:ajv@8.11.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz",
"integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"dependencies": {
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}
}
},
"ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",

View File

@ -20,12 +20,16 @@ import {
TableHeader,
TableHead,
TableRow,
TimePicker,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
import {
convertDateStringToSeconds,
convertSecondsToFormattedDate,
convertDateAndTimeStringsToSeconds,
convertDateObjectToFormattedHoursMinutes,
convertSecondsToFormattedDateString,
convertSecondsToFormattedDateTime,
convertSecondsToFormattedTimeHoursMinutes,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
@ -49,6 +53,10 @@ type OwnProps = {
perPageOptions?: number[];
};
interface dateParameters {
[key: string]: ((..._args: any[]) => any)[];
}
export default function ProcessInstanceListTable({
filtersEnabled = true,
processModelFullIdentifier,
@ -66,11 +74,20 @@ export default function ProcessInstanceListTable({
const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFrom, setStartFrom] = useState<string>('');
const [startTo, setStartTo] = useState<string>('');
const [endFrom, setEndFrom] = useState<string>('');
const [endTo, setEndTo] = useState<string>('');
const [startFromDate, setStartFromDate] = useState<string>('');
const [startToDate, setStartToDate] = useState<string>('');
const [endFromDate, setEndFromDate] = useState<string>('');
const [endToDate, setEndToDate] = useState<string>('');
const [startFromTime, setStartFromTime] = useState<string>('');
const [startToTime, setStartToTime] = useState<string>('');
const [endFromTime, setEndFromTime] = useState<string>('');
const [endToTime, setEndToTime] = useState<string>('');
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
const [startFromTimeInvalid, setStartFromTimeInvalid] =
useState<boolean>(false);
const [startToTimeInvalid, setStartToTimeInvalid] = useState<boolean>(false);
const [endFromTimeInvalid, setEndFromTimeInvalid] = useState<boolean>(false);
const [endToTimeInvalid, setEndToTimeInvalid] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
@ -86,14 +103,23 @@ export default function ProcessInstanceListTable({
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const parametersToAlwaysFilterBy = useMemo(() => {
const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return {
start_from: setStartFrom,
start_to: setStartTo,
end_from: setEndFrom,
end_to: setEndTo,
start_from: [setStartFromDate, setStartFromTime],
start_to: [setStartToDate, setStartToTime],
end_from: [setEndFromDate, setEndFromTime],
end_to: [setEndToDate, setEndToTime],
};
}, [setStartFrom, setStartTo, setEndFrom, setEndTo]);
}, [
setStartFromDate,
setStartFromTime,
setStartToDate,
setStartToTime,
setEndFromDate,
setEndFromTime,
setEndToDate,
setEndToTime,
]);
const parametersToGetFromSearchParams = useMemo(() => {
return {
@ -130,19 +156,27 @@ export default function ProcessInstanceListTable({
queryParamString += `&user_filter=${userAppliedFilter}`;
}
Object.keys(parametersToAlwaysFilterBy).forEach((paramName: string) => {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToAlwaysFilterBy[paramName];
Object.keys(dateParametersToAlwaysFilterBy).forEach(
(paramName: string) => {
const dateFunctionToCall =
dateParametersToAlwaysFilterBy[paramName][0];
const timeFunctionToCall =
dateParametersToAlwaysFilterBy[paramName][1];
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
const dateString = convertSecondsToFormattedDate(
const dateString = convertSecondsToFormattedDateString(
searchParamValue as any
);
functionToCall(dateString);
dateFunctionToCall(dateString);
const timeString = convertSecondsToFormattedTimeHoursMinutes(
searchParamValue as any
);
timeFunctionToCall(timeString);
setShowFilterOptions(true);
}
});
}
);
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
@ -211,7 +245,7 @@ export default function ProcessInstanceListTable({
params,
oneMonthInSeconds,
oneHourInSeconds,
parametersToAlwaysFilterBy,
dateParametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
filtersEnabled,
paginationQueryParamPrefix,
@ -219,16 +253,25 @@ export default function ProcessInstanceListTable({
perPageOptions,
]);
// This sets the filter data using the saved reports returned from the initial instance_list query.
// This could probably be merged into the main useEffect but it works here now.
useEffect(() => {
const filters = processInstanceFilters as any;
Object.keys(parametersToAlwaysFilterBy).forEach((paramName: string) => {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToAlwaysFilterBy[paramName];
Object.keys(dateParametersToAlwaysFilterBy).forEach((paramName: string) => {
const dateFunctionToCall = dateParametersToAlwaysFilterBy[paramName][0];
const timeFunctionToCall = dateParametersToAlwaysFilterBy[paramName][1];
const paramValue = filters[paramName];
functionToCall('');
dateFunctionToCall('');
timeFunctionToCall('');
if (paramValue) {
const dateString = convertSecondsToFormattedDate(paramValue as any);
functionToCall(dateString);
const dateString = convertSecondsToFormattedDateString(
paramValue as any
);
dateFunctionToCall(dateString);
const timeString = convertSecondsToFormattedTimeHoursMinutes(
paramValue as any
);
timeFunctionToCall(timeString);
setShowFilterOptions(true);
}
});
@ -253,7 +296,7 @@ export default function ProcessInstanceListTable({
setProcessStatusSelection(processStatusSelectedArray);
}, [
processInstanceFilters,
parametersToAlwaysFilterBy,
dateParametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
processModelAvailableItems,
]);
@ -285,10 +328,22 @@ export default function ProcessInstanceListTable({
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
const startFromSeconds = convertDateStringToSeconds(startFrom);
const endFromSeconds = convertDateStringToSeconds(endFrom);
const startToSeconds = convertDateStringToSeconds(startTo);
const endToSeconds = convertDateStringToSeconds(endTo);
const startFromSeconds = convertDateAndTimeStringsToSeconds(
startFromDate,
startFromTime
);
const startToSeconds = convertDateAndTimeStringsToSeconds(
startToDate,
startToTime
);
const endFromSeconds = convertDateAndTimeStringsToSeconds(
endFromDate,
endFromTime
);
const endToSeconds = convertDateAndTimeStringsToSeconds(
endToDate,
endToTime
);
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
@ -342,9 +397,14 @@ export default function ProcessInstanceListTable({
labelString: any,
name: any,
initialDate: any,
onChangeFunction: any
initialTime: string,
onChangeDateFunction: any,
onChangeTimeFunction: any,
timeInvalid: boolean,
setTimeInvalid: any
) => {
return (
<>
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
@ -355,11 +415,32 @@ export default function ProcessInstanceListTable({
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
onChangeFunction(dateChangeEvent.srcElement.value);
if (!initialDate && !initialTime) {
onChangeTimeFunction(
convertDateObjectToFormattedHoursMinutes(new Date())
);
}
onChangeDateFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
<TimePicker
invalid={timeInvalid}
id="time-picker"
labelText="Select a time"
pattern="^([01]\d|2[0-3]):?([0-5]\d)$"
value={initialTime}
onChange={(event: any) => {
if (event.srcElement.validity.valid) {
setTimeInvalid(false);
} else {
setTimeInvalid(true);
}
onChangeTimeFunction(event.srcElement.value);
}}
/>
</>
);
};
@ -386,10 +467,14 @@ export default function ProcessInstanceListTable({
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFrom('');
setStartTo('');
setEndFrom('');
setEndTo('');
setStartFromDate('');
setStartFromTime('');
setStartToDate('');
setStartToTime('');
setEndFromDate('');
setEndFromTime('');
setEndToDate('');
setEndToTime('');
};
const filterOptions = () => {
@ -415,18 +500,49 @@ export default function ProcessInstanceListTable({
{dateComponent(
'Start date from',
'start-from',
startFrom,
setStartFrom
startFromDate,
startFromTime,
setStartFromDate,
setStartFromTime,
startFromTimeInvalid,
setStartFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent('Start date to', 'start-to', startTo, setStartTo)}
{dateComponent(
'Start date to',
'start-to',
startToDate,
startToTime,
setStartToDate,
setStartToTime,
startToTimeInvalid,
setStartToTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent('End date from', 'end-from', endFrom, setEndFrom)}
{dateComponent(
'End date from',
'end-from',
endFromDate,
endFromTime,
setEndFromDate,
setEndFromTime,
endFromTimeInvalid,
setEndFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent('End date to', 'end-to', endTo, setEndTo)}
{dateComponent(
'End date to',
'end-to',
endToDate,
endToTime,
setEndToDate,
setEndToTime,
endToTimeInvalid,
setEndToTimeInvalid
)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
@ -493,7 +609,7 @@ export default function ProcessInstanceListTable({
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDate(seconds) || '-';
return convertSecondsToFormattedDateTime(seconds) || '-';
};
const defaultFormatter = (_row: any, value: any) => {
return value;

View File

@ -18,5 +18,6 @@ export const PROCESS_STATUSES = [
// with time: yyyy-MM-dd HH:mm:ss
export const DATE_TIME_FORMAT = 'yyyy-MM-dd HH:mm:ss';
export const TIME_FORMAT_HOURS_MINUTES = 'HH:mm';
export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_CARBON = 'Y-m-d';

View File

@ -1,4 +1,4 @@
import { convertSecondsToFormattedDate, slugifyString } from './helpers';
import { convertSecondsToFormattedDateString, slugifyString } from './helpers';
test('it can slugify a string', () => {
expect(slugifyString('hello---world_ and then Some such-')).toEqual(
@ -7,6 +7,6 @@ test('it can slugify a string', () => {
});
test('it can keep the correct date when converting seconds to date', () => {
const dateString = convertSecondsToFormattedDate(1666325400);
const dateString = convertSecondsToFormattedDateString(1666325400);
expect(dateString).toEqual('2022-10-21');
});

View File

@ -1,5 +1,9 @@
import { format } from 'date-fns';
import { DATE_TIME_FORMAT, DATE_FORMAT } from './config';
import {
DATE_TIME_FORMAT,
DATE_FORMAT,
TIME_FORMAT_HOURS_MINUTES,
} from './config';
import {
DEFAULT_PER_PAGE,
DEFAULT_PAGE,
@ -42,27 +46,72 @@ export const convertDateToSeconds = (
return null;
};
export const convertDateObjectToFormattedString = (dateObject: Date) => {
if (dateObject) {
return format(dateObject, DATE_FORMAT);
}
return null;
};
export const convertDateAndTimeStringsToDate = (
dateString: string,
timeString: string
) => {
if (dateString && timeString) {
return new Date(`${dateString}T${timeString}`);
}
return null;
};
export const convertDateAndTimeStringsToSeconds = (
dateString: string,
timeString: string
) => {
const dateObject = convertDateAndTimeStringsToDate(dateString, timeString);
if (dateObject) {
return convertDateToSeconds(dateObject);
}
return null;
};
export const convertStringToDate = (dateString: string) => {
if (dateString) {
// add midnight time to the date so it c uses the correct date
// after converting to timezone
return new Date(`${dateString}T00:10:00`);
return convertDateAndTimeStringsToSeconds(dateString, '00:10:00');
};
export const convertSecondsToDateObject = (seconds: number) => {
if (seconds) {
return new Date(seconds * 1000);
}
return null;
};
export const convertSecondsToFormattedDateTime = (seconds: number) => {
if (seconds) {
const dateObject = new Date(seconds * 1000);
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return format(dateObject, DATE_TIME_FORMAT);
}
return null;
};
export const convertSecondsToFormattedDate = (seconds: number) => {
if (seconds) {
const dateObject = new Date(seconds * 1000);
return format(dateObject, DATE_FORMAT);
export const convertDateObjectToFormattedHoursMinutes = (dateObject: Date) => {
if (dateObject) {
return format(dateObject, TIME_FORMAT_HOURS_MINUTES);
}
return null;
};
export const convertSecondsToFormattedTimeHoursMinutes = (seconds: number) => {
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return convertDateObjectToFormattedHoursMinutes(dateObject);
}
return null;
};
export const convertSecondsToFormattedDateString = (seconds: number) => {
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return convertDateObjectToFormattedString(dateObject);
}
return null;
};

View File

@ -5,7 +5,7 @@ import { Link, useParams, useSearchParams } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
convertSecondsToFormattedDate,
convertSecondsToFormattedDateString,
getPageInfoFromSearchParams,
modifyProcessModelPath,
unModifyProcessModelPath,
@ -68,7 +68,9 @@ export default function MessageInstanceList() {
<td>{rowToUse.failure_cause || '-'}</td>
<td>{rowToUse.status}</td>
<td>
{convertSecondsToFormattedDate(rowToUse.created_at_in_seconds)}
{convertSecondsToFormattedDateString(
rowToUse.created_at_in_seconds
)}
</td>
</tr>
);

View File

@ -6,7 +6,7 @@ import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
getPageInfoFromSearchParams,
convertSecondsToFormattedDate,
convertSecondsToFormattedDateString,
modifyProcessModelPath,
unModifyProcessModelPath,
} from '../helpers';
@ -49,7 +49,7 @@ export default function ProcessInstanceLogList() {
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
>
{convertSecondsToFormattedDate(rowToUse.timestamp)}
{convertSecondsToFormattedDateString(rowToUse.timestamp)}
</Link>
</td>
</tr>

View File

@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
// FIXME: npm install @rjsf/validator-ajv8 and use it as soon as
// rawErrors is fixed.
@ -9,8 +9,14 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
// https://github.com/rjsf-team/react-jsonschema-form/blob/main/docs/api-reference/uiSchema.md talks about rawErrors
import validator from '@rjsf/validator-ajv6';
import {
TabList,
Tab,
Tabs,
Grid,
Column,
// @ts-ignore
import { Button, Stack } from '@carbon/react';
} from '@carbon/react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@ -73,39 +79,52 @@ export default function TaskShow() {
const buildTaskNavigation = () => {
let userTasksElement;
let selectedTabIndex = 0;
if (userTasks) {
userTasksElement = (userTasks as any).map(function getUserTasksElement(
userTask: any
userTask: any,
index: number
) {
const taskUrl = `/tasks/${params.process_instance_id}/${userTask.id}`;
if (userTask.id === params.task_id) {
return <span>{userTask.name}</span>;
selectedTabIndex = index;
return <Tab selected>{userTask.title}</Tab>;
}
if (userTask.state === 'COMPLETED') {
return (
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}>
{userTask.name}
</Link>
<Tab
onClick={() => navigate(taskUrl)}
data-qa={`form-nav-${userTask.name}`}
>
{userTask.title}
</Tab>
);
}
if (userTask.state === 'FUTURE') {
return <span style={{ color: 'red' }}>{userTask.name}</span>;
return <Tab disabled>{userTask.title}</Tab>;
}
if (userTask.state === 'READY') {
return (
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}>
{userTask.name} - Current
</Link>
<Tab
onClick={() => navigate(taskUrl)}
data-qa={`form-nav-${userTask.name}`}
>
{userTask.title}
</Tab>
);
}
return null;
});
}
return (
<Stack orientation="horizontal" gap={3}>
<Button href="/tasks">Go Back To List</Button>
<Tabs
title="Steps in this process instance involving people"
selectedIndex={selectedTabIndex}
>
<TabList aria-label="List of tabs" contained>
{userTasksElement}
</Stack>
</TabList>
</Tabs>
);
};
@ -149,6 +168,8 @@ export default function TaskShow() {
}
return (
<Grid fullWidth condensed>
<Column md={5} lg={8} sm={4}>
<Form
formData={taskData}
onSubmit={handleFormSubmit}
@ -158,6 +179,8 @@ export default function TaskShow() {
>
{reactFragmentToHideSubmitButton}
</Form>
</Column>
</Grid>
);
};
@ -175,7 +198,7 @@ export default function TaskShow() {
);
};
if (task) {
if (task && userTasks) {
const taskToUse = task as any;
let statusString = '';
if (taskToUse.state !== 'READY') {

View File

@ -82,6 +82,9 @@ export default function BaseInputTemplate<
} else if (schema && schema.title) {
labelToUse = schema.title;
}
if (required) {
labelToUse = `${labelToUse}*`;
}
let invalid = false;
let errorMessageForField = null;