From 4f01052cc6b3d12131a6dcaeab2d9b2a9ef76951 Mon Sep 17 00:00:00 2001 From: Gianluca Frediani Date: Fri, 25 Oct 2024 14:19:54 +0200 Subject: [PATCH 1/3] fix: added validation of date format for ticket field prefilling --- .../usePrefilledTicketFields.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/modules/new-request-form/usePrefilledTicketFields.tsx b/src/modules/new-request-form/usePrefilledTicketFields.tsx index b7ab45f5d..659ab7fe7 100644 --- a/src/modules/new-request-form/usePrefilledTicketFields.tsx +++ b/src/modules/new-request-form/usePrefilledTicketFields.tsx @@ -4,6 +4,7 @@ import DOMPurify from "dompurify"; const MAX_URL_LENGTH = 2048; const TICKET_FIELD_PREFIX = "tf_"; +const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; const ALLOWED_BOOLEAN_VALUES = ["true", "false"]; const ALLOWED_HTML_TAGS = [ @@ -57,6 +58,20 @@ function getFieldFromId(id: string, prefilledTicketFields: Fields) { } } +function isValidDate(dateString: string) { + if (!DATE_REGEX.test(dateString)) { + return false; + } + + const date = new Date(dateString); + const [year, month, day] = dateString.split("-").map(Number); + return ( + date.getUTCFullYear() === year && + date.getUTCMonth() + 1 === month && + date.getUTCDate() === day + ); +} + function getPrefilledTicketFields(fields: Fields): Fields { const { href } = location; const params = new URL(href).searchParams; @@ -102,6 +117,12 @@ function getPrefilledTicketFields(fields: Fields): Fields { : ""; } break; + case "due_at": + case "date": + if (isValidDate(sanitizedValue)) { + field.value = sanitizedValue; + } + break; default: field.value = sanitizedValue; } From 67fd3772684fcdb1055ecf2fd91c9cd64c38cf25 Mon Sep 17 00:00:00 2001 From: Gianluca Frediani Date: Fri, 25 Oct 2024 14:22:01 +0200 Subject: [PATCH 2/3] fix: fixed date fields prefilling with wrong dates in some timezones --- .../new-request-form/fields/DatePicker.tsx | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/modules/new-request-form/fields/DatePicker.tsx b/src/modules/new-request-form/fields/DatePicker.tsx index 5c8f6c643..88068fe06 100644 --- a/src/modules/new-request-form/fields/DatePicker.tsx +++ b/src/modules/new-request-form/fields/DatePicker.tsx @@ -9,7 +9,7 @@ import { import { Span } from "@zendeskgarden/react-typography"; import type { Field } from "../data-types"; import type { ChangeEventHandler } from "react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; interface DatePickerProps { field: Field; @@ -29,7 +29,26 @@ export function DatePicker({ value ? new Date(value as string) : undefined ); - const formatDate = (value: Date | undefined) => { + const dateTimeFormat = useMemo( + () => + new Intl.DateTimeFormat(locale, { + month: "long", + day: "numeric", + year: "numeric", + timeZone: "UTC", + }), + [locale] + ); + + /* Formats the date using the UTC time zone to prevent timezone-related issues. + * By creating a new Date object with only the date, the time defaults to 00:00:00 UTC. + * This avoids date shifts that can occur if formatted with the local time zone, as Garden does by default. + */ + const formatDateInput = (date: Date) => { + return dateTimeFormat.format(date); + }; + + const formatDateValue = (value: Date | undefined) => { if (value === undefined) { return ""; } @@ -43,7 +62,7 @@ export function DatePicker({ Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0) ) as Date; setDate(newDate); - const dateString = formatDate(newDate); + const dateString = formatDateValue(newDate); if (dateString !== undefined) { onChange(dateString); } @@ -66,7 +85,12 @@ export function DatePicker({ {description && ( )} - + {error && {error}} - + ); } From 34ce8e9c3d4591a574d1ab2d0a4413c7be73cba6 Mon Sep 17 00:00:00 2001 From: Gianluca Frediani Date: Tue, 29 Oct 2024 15:35:41 +0100 Subject: [PATCH 3/3] refactor: added useCallback for datepicker callbacks --- .../new-request-form/NewRequestForm.tsx | 16 +++-- .../new-request-form/fields/DatePicker.tsx | 66 ++++++++++--------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/modules/new-request-form/NewRequestForm.tsx b/src/modules/new-request-form/NewRequestForm.tsx index dc5304c3c..71d55457c 100644 --- a/src/modules/new-request-form/NewRequestForm.tsx +++ b/src/modules/new-request-form/NewRequestForm.tsx @@ -120,6 +120,7 @@ export function NewRequestForm({ organizations.length > 0 && organizations[0]?.id ? organizations[0]?.id?.toString() : null; + const handleChange = useCallback( (field: Field, value: Field["value"]) => { setTicketFields( @@ -141,13 +142,16 @@ export function NewRequestForm({ setOrganizationField({ ...organizationField, value }); } - function handleDueDateChange(value: string) { - if (dueDateField === null) { - return; - } + const handleDueDateChange = useCallback( + (value: string) => { + if (dueDateField === null) { + return; + } - setDueDateField({ ...dueDateField, value }); - } + setDueDateField({ ...dueDateField, value }); + }, + [dueDateField] + ); return ( <> diff --git a/src/modules/new-request-form/fields/DatePicker.tsx b/src/modules/new-request-form/fields/DatePicker.tsx index 88068fe06..ac63243f4 100644 --- a/src/modules/new-request-form/fields/DatePicker.tsx +++ b/src/modules/new-request-form/fields/DatePicker.tsx @@ -9,7 +9,7 @@ import { import { Span } from "@zendeskgarden/react-typography"; import type { Field } from "../data-types"; import type { ChangeEventHandler } from "react"; -import { useMemo, useState } from "react"; +import { useCallback, useState } from "react"; interface DatePickerProps { field: Field; @@ -29,44 +29,48 @@ export function DatePicker({ value ? new Date(value as string) : undefined ); - const dateTimeFormat = useMemo( - () => - new Intl.DateTimeFormat(locale, { + /* Formats the date using the UTC time zone to prevent timezone-related issues. + * By creating a new Date object with only the date, the time defaults to 00:00:00 UTC. + * This avoids date shifts that can occur if formatted with the local time zone, as Garden does by default. + */ + const formatDateInput = useCallback( + (date: Date) => { + const dateTimeFormat = new Intl.DateTimeFormat(locale, { month: "long", day: "numeric", year: "numeric", timeZone: "UTC", - }), + }); + return dateTimeFormat.format(date); + }, [locale] ); - /* Formats the date using the UTC time zone to prevent timezone-related issues. - * By creating a new Date object with only the date, the time defaults to 00:00:00 UTC. - * This avoids date shifts that can occur if formatted with the local time zone, as Garden does by default. - */ - const formatDateInput = (date: Date) => { - return dateTimeFormat.format(date); - }; - - const formatDateValue = (value: Date | undefined) => { - if (value === undefined) { - return ""; - } - const isoString = value.toISOString(); - return valueFormat === "dateTime" ? isoString : isoString.split("T")[0]; - }; + const formatDateValue = useCallback( + (value: Date | undefined) => { + if (value === undefined) { + return ""; + } + const isoString = value.toISOString(); + return valueFormat === "dateTime" ? isoString : isoString.split("T")[0]; + }, + [valueFormat] + ); - const handleChange = (date: Date) => { - // Set the time to 12:00:00 as this is also the expected behavior across Support and the API - const newDate = new Date( - Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0) - ) as Date; - setDate(newDate); - const dateString = formatDateValue(newDate); - if (dateString !== undefined) { - onChange(dateString); - } - }; + const handleChange = useCallback( + (date: Date) => { + // Set the time to 12:00:00 as this is also the expected behavior across Support and the API + const newDate = new Date( + Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0) + ) as Date; + setDate(newDate); + const dateString = formatDateValue(newDate); + if (dateString !== undefined) { + onChange(dateString); + } + }, + [onChange, formatDateValue] + ); const handleInputChange: ChangeEventHandler = (e) => { // Allow field to be cleared