@ministryofjustice/hmpps-digital-prison-reporting-frontend
Version:
The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.
160 lines (157 loc) • 5.76 kB
JavaScript
import { z } from 'zod';
import dayjs from 'dayjs';
import isSameOrBefore from '../../_virtual/isSameOrBefore.js';
import { isValidUiDate, UI_INPUT_FORMATS } from '../utils/dateHelper.js';
dayjs.extend(isSameOrBefore);
const buildFilterValidator = (field) => {
if (!field.filter) {
return z.any();
}
switch (field.filter.type) {
case 'multiselect':
case 'autocompletemulti':
return buildMultiSelectField(field);
case 'date':
return buildDateField(field);
case 'daterange':
case 'granulardaterange':
return buildDateRangeField(field);
case 'Radio':
case 'Select':
case 'autocomplete':
return buildSelectField(field);
case 'text':
default:
return buildTextField(field);
}
};
/**
* TEXT / STRING FIELDS
*/
const buildTextField = (field) => {
const { filter, display } = field;
if (!filter)
throw new Error('Missing filter');
let stringSchema = z.string().trim();
if (filter.mandatory) {
stringSchema = stringSchema.min(1, `${display} is required`);
}
if (filter.pattern) {
const regex = new RegExp(filter.pattern);
stringSchema = stringSchema.refine(value => value === '' || regex.test(value), {
message: `${display} has an invalid format`,
});
}
return z.preprocess(value => value ?? '', stringSchema);
};
/**
* MULTI‑SELECT FIELDS
*/
const buildMultiSelectField = (field) => {
const { filter, display } = field;
if (!filter)
throw new Error('Missing filter');
const validValues = (filter.staticOptions ?? []).map(o => o.name);
let arraySchema = z.array(z.string());
// Valid option check
arraySchema = arraySchema.refine(values => values.every(v => validValues.includes(v)), {
message: `${display} contains invalid selections`,
});
// Mandatory
if (filter.mandatory) {
arraySchema = arraySchema.min(1, `${display} Please select at least one`);
}
// minSelected
if (typeof filter.minSelected === 'number') {
arraySchema = arraySchema.min(filter.minSelected, `${display} Select at least ${filter.minSelected}`);
}
// maxSelected
if (typeof filter.maxSelected === 'number') {
arraySchema = arraySchema.max(filter.maxSelected, `${display} Select no more than ${filter.maxSelected}`);
}
return z.preprocess(value => {
if (value == null)
return [];
if (Array.isArray(value))
return value;
return value
.toString()
.split(',')
.map((v) => v.trim())
.filter(Boolean); // removes empty strings
}, arraySchema);
};
/**
* SELECT FIELDS
*/
const buildSelectField = (field) => {
const { filter, display } = field;
if (!filter)
throw new Error('Missing filter');
const validValues = (filter.staticOptions ?? []).map(o => o.name);
let schema = z.string().trim();
schema = schema.refine(value => value === 'no-filter' || value === '' || validValues.includes(value), {
message: `${display} has an invalid selection`,
});
if (filter.mandatory) {
schema = schema.refine(value => value !== '' && value !== 'no-filter', `${display} is required`);
}
return z.preprocess(v => v ?? '', schema);
};
/**
* DATE FIELDS
*/
const buildDateField = (field) => {
const { filter, display } = field;
if (!filter)
throw new Error('Missing filter');
const schema = z.preprocess(v => (v === '' ? undefined : v), z.string().refine(isValidUiDate, `${display} must be a valid date`).optional());
return filter.mandatory ? schema.refine(v => v !== undefined, `${display} is required`) : schema;
};
/**
* DATE RANGE FIELDS
*/
const buildDateRangeField = (field) => {
const { filter, display } = field;
if (!filter)
throw new Error('Missing filter');
if (filter.type === 'daterange' || filter.type === 'granulardaterange') {
const isMandatory = filter.mandatory === true;
let base = z.object({
start: isMandatory
? z
.string()
.min(1, `${display} start date is required`)
.refine(isValidUiDate, `${display} start date must be a valid date`)
: z
.string()
.optional()
.refine(value => !value || isValidUiDate(value), `${display} start date must be a valid date`),
end: isMandatory
? z
.string()
.min(1, `${display} end date is required`)
.refine(isValidUiDate, `${display} end date must be a valid date`)
: z
.string()
.optional()
.refine(value => !value || isValidUiDate(value), `${display} end date must be a valid date`),
});
// Cross-field validation:
// Only enforce ordering when both dates are present
base = base.refine(({ start, end }) => {
if (!start || !end)
return true;
const startDate = dayjs(start, [...UI_INPUT_FORMATS], true);
const endDate = dayjs(end, [...UI_INPUT_FORMATS], true);
return startDate.isSameOrBefore(endDate);
}, {
message: `${display} end date must be the same as or after the start date`,
path: ['end'],
});
return base;
}
throw new Error(`Unsupported filter type: ${filter.type}`);
};
export { buildDateField, buildDateRangeField, buildFilterValidator };
//# sourceMappingURL=filterValidators.js.map