mattermost-redux
Version:
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
185 lines (184 loc) • 7.31 kB
JavaScript
;
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkDialogElementForError = checkDialogElementForError;
exports.checkIfErrorsMatchElements = checkIfErrorsMatchElements;
const date_fns_1 = require("date-fns");
const react_intl_1 = require("react-intl");
// Validation patterns for exact storage format matching
const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD
const DATETIME_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/; // YYYY-MM-DDTHH:mm:ssZ or with offset
// Relative pattern: [+-]NNN[dwmHMS]
const RELATIVE_PATTERN = /^([+-]\d{1,3})([dwmHMS])$/;
/**
* Resolves a min_date/max_date bound string to a Date.
* Handles relative patterns (+2H, +30M, +7d, etc.) and ISO date/datetime strings.
* Returns null if the value cannot be resolved.
*/
function resolveBoundToDate(value) {
// Named relative words
if (value === 'today') {
return (0, date_fns_1.startOfDay)(new Date());
}
if (value === 'tomorrow') {
return (0, date_fns_1.startOfDay)((0, date_fns_1.addDays)(new Date(), 1));
}
if (value === 'yesterday') {
return (0, date_fns_1.startOfDay)((0, date_fns_1.addDays)(new Date(), -1));
}
// Dynamic relative patterns: +2H, +30M, +7d, etc.
const match = value.match(RELATIVE_PATTERN);
if (match) {
const amount = parseInt(match[1], 10);
const unit = match[2];
const now = new Date();
switch (unit) {
case 'd': return (0, date_fns_1.startOfDay)((0, date_fns_1.addDays)(now, amount));
case 'w': return (0, date_fns_1.startOfDay)((0, date_fns_1.addWeeks)(now, amount));
case 'm': return (0, date_fns_1.startOfDay)((0, date_fns_1.addMonths)(now, amount));
case 'H': return (0, date_fns_1.addHours)(now, amount);
case 'M': return (0, date_fns_1.addMinutes)(now, amount);
case 'S': return (0, date_fns_1.addSeconds)(now, amount);
default: return null;
}
}
const parsed = (0, date_fns_1.parseISO)(value);
return (0, date_fns_1.isValid)(parsed) ? parsed : null;
}
/**
* Validates date/datetime field values for format and range constraints
*/
function validateDateTimeValue(value, elem) {
const parsedDate = (0, date_fns_1.parseISO)(value);
if (!(0, date_fns_1.isValid)(parsedDate)) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.bad_format',
defaultMessage: 'Invalid date format',
});
}
const isDateField = elem.type === 'date';
if (isDateField) {
if (!DATE_FORMAT_PATTERN.test(value)) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.bad_date_format',
defaultMessage: 'Date field must be in YYYY-MM-DD format',
});
}
}
else if (!DATETIME_FORMAT_PATTERN.test(value)) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.bad_datetime_format',
defaultMessage: 'DateTime field must be in YYYY-MM-DDTHH:mm:ssZ or YYYY-MM-DDTHH:mm:ss+HH:MM format',
});
}
// Range validation against min_date / max_date
if (elem.min_date) {
const minDate = resolveBoundToDate(elem.min_date);
if (minDate && parsedDate < minDate) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.before_min_date',
defaultMessage: 'Selected time is before the minimum allowed date.',
});
}
}
if (elem.max_date) {
const maxDate = resolveBoundToDate(elem.max_date);
if (maxDate && parsedDate > maxDate) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.after_max_date',
defaultMessage: 'Selected time is after the maximum allowed date.',
});
}
}
return null;
}
function checkDialogElementForError(elem, value) {
// Check if value is empty (handles arrays for multiselect)
let isEmpty;
if (value === 0) {
isEmpty = false;
}
else if (Array.isArray(value)) {
isEmpty = value.length === 0;
}
else {
isEmpty = !value;
}
if (isEmpty && !elem.optional) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.required',
defaultMessage: 'This field is required.',
});
}
const type = elem.type;
if (type === 'text' || type === 'textarea') {
if (value && value.length < elem.min_length) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.too_short',
// minLength provided by InteractiveDialog
// eslint-disable-next-line formatjs/enforce-placeholders
defaultMessage: 'Minimum input length is {minLength}.',
});
}
if (elem.subtype === 'email') {
if (value && !value.includes('@')) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.bad_email',
defaultMessage: 'Must be a valid email address.',
});
}
}
if (elem.subtype === 'number') {
if (value && isNaN(value)) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.bad_number',
defaultMessage: 'Must be a number.',
});
}
}
if (elem.subtype === 'url') {
if (value && !value.includes('http://') && !value.includes('https://')) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.bad_url',
defaultMessage: 'URL must include http:// or https://.',
});
}
}
}
else if (type === 'radio') {
const options = elem.options;
if (typeof value !== 'undefined' && Array.isArray(options) && !options.some((e) => e.value === value)) {
return (0, react_intl_1.defineMessage)({
id: 'interactive_dialog.error.invalid_option',
defaultMessage: 'Must be a valid option',
});
}
}
else if (type === 'date' || type === 'datetime') {
// Validate date/datetime format and range constraints
if (value && typeof value === 'string') {
const validationError = validateDateTimeValue(value, elem);
if (validationError) {
return validationError;
}
}
return null;
}
return null;
}
// If we're returned errors that don't match any of the elements we have,
// ignore them and complete the dialog
function checkIfErrorsMatchElements(errors = {}, elements = []) {
for (const name in errors) {
if (!Object.hasOwn(errors, name)) {
continue;
}
for (const elem of elements) {
if (elem.name === name) {
return true;
}
}
}
return false;
}