diff --git a/docs/admin/overview.mdx b/docs/admin/overview.mdx index a02a7d94e92..ebe877da672 100644 --- a/docs/admin/overview.mdx +++ b/docs/admin/overview.mdx @@ -99,6 +99,7 @@ The following options are available: | **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). | | **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `` tag. Defaults to `false`. | | **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. | +| **`timezone`** | Configure the timezone settings for the admin panel. [More details](#timezones) | | **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). | @@ -242,3 +243,21 @@ The Payload Admin Panel is translated in over [30 languages and counting](https: ## Light and Dark Modes Users in the Admin Panel have the ability to choose between light mode and dark mode for their editing experience. Users can select their preferred theme from their account page. Once selected, it is saved to their user's preferences and persisted across sessions and devices. If no theme was selected, the Admin Panel will automatically detect the operation system's theme and use that as the default. + +## Timezones + +The `admin.timezone` configuration allows you to configure timezone settings for the Admin Panel. You can customise the available list of timezones and in the future configure the default timezone for the Admin Panel and for all users. + +The following options are available: + +| Option | Description | +| ----------------- | ----------------------------------------------- | +| `supportedTimezones` | An array of label/value options for selectable timezones where the value is the IANA name eg. `America/Detroit` | +| `defaultTimezone` | The `value` of the default selected timezone. eg. `America/Los_Angeles` | + +We validate the supported timezones array by checking the value against the list of IANA timezones supported via the Intl API, specifically `Intl.supportedValuesOf('timeZone')`. + + + **Important** + You must enable timezones on each individual date field via `timezone: true`. See [Date Fields](../fields/overview#date) for more information. + diff --git a/docs/fields/date.mdx b/docs/fields/date.mdx index fb75797bf5d..547cd9d07d7 100644 --- a/docs/fields/date.mdx +++ b/docs/fields/date.mdx @@ -43,6 +43,7 @@ export const MyDateField: Field = { | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| **`timezone`** * | Set to `true` to enable timezone selection on this field. [More details](#timezones). | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | @@ -222,3 +223,23 @@ export const CustomDateFieldLabelClient: DateFieldLabelClientComponent = ({ } ``` +## Timezones + +To enable timezone selection on a Date field, set the `timezone` property to `true`: + +```ts +{ + name: 'date', + type: 'date', + timezone: true, +} +``` + +This will add a dropdown to the date picker that allows users to select a timezone. The selected timezone will be saved in the database along with the date in a new column named `date_tz`. + +You can customise the available list of timezones in the [global admin config](../admin/overview#timezones). + + + **Good to know:** + The date itself will be stored in UTC so it's up to you to handle the conversion to the user's timezone when displaying the date in your frontend. + diff --git a/packages/payload/src/config/client.ts b/packages/payload/src/config/client.ts index 175c100618e..7c9fc5c7811 100644 --- a/packages/payload/src/config/client.ts +++ b/packages/payload/src/config/client.ts @@ -97,6 +97,7 @@ export const createClientConfig = ({ meta: config.admin.meta, routes: config.admin.routes, theme: config.admin.theme, + timezone: config.admin.timezone, user: config.admin.user, } if (config.admin.livePreview) { diff --git a/packages/payload/src/config/sanitize.ts b/packages/payload/src/config/sanitize.ts index 9603760408e..c755d63e9a4 100644 --- a/packages/payload/src/config/sanitize.ts +++ b/packages/payload/src/config/sanitize.ts @@ -9,6 +9,7 @@ import type { LocalizationConfigWithLabels, LocalizationConfigWithNoLabels, SanitizedConfig, + Timezone, } from './types.js' import { defaultUserCollection } from '../auth/defaultUser.js' @@ -16,6 +17,7 @@ import { authRootEndpoints } from '../auth/endpoints/index.js' import { sanitizeCollection } from '../collections/config/sanitize.js' import { migrationsCollection } from '../database/migrations/migrationsCollection.js' import { DuplicateCollection, InvalidConfiguration } from '../errors/index.js' +import { defaultTimezones } from '../fields/baseFields/timezone/defaultTimezones.js' import { sanitizeGlobal } from '../globals/config/sanitize.js' import { getLockedDocumentsCollection } from '../lockedDocuments/lockedDocumentsCollection.js' import getPreferencesCollection from '../preferences/preferencesCollection.js' @@ -56,6 +58,32 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial ) } + if (sanitizedConfig?.admin?.timezone) { + if (typeof sanitizedConfig?.admin?.timezone?.supportedTimezones === 'function') { + sanitizedConfig.admin.timezone.supportedTimezones = + sanitizedConfig.admin.timezone.supportedTimezones({ defaultTimezones }) + } + + if (!sanitizedConfig?.admin?.timezone?.supportedTimezones) { + sanitizedConfig.admin.timezone.supportedTimezones = defaultTimezones + } + } else { + sanitizedConfig.admin.timezone = { + supportedTimezones: defaultTimezones, + } + } + // Timezones supported by the Intl API + const _internalSupportedTimezones = Intl.supportedValuesOf('timeZone') + + // We're casting here because it's already been sanitised above but TS still thinks it could be a function + ;(sanitizedConfig.admin.timezone.supportedTimezones as Timezone[]).forEach((timezone) => { + if (!_internalSupportedTimezones.includes(timezone.value)) { + throw new InvalidConfiguration( + `Timezone ${timezone.value} is not supported by the current runtime via the Intl API.`, + ) + } + }) + return sanitizedConfig as unknown as Partial } diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 9a3dea3ac98..f74981d5f84 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -426,6 +426,32 @@ export const serverProps: (keyof ServerProps)[] = [ 'permissions', ] +export type Timezone = { + label: string + value: string +} + +type SupportedTimezonesFn = (args: { defaultTimezones: Timezone[] }) => Timezone[] + +type TimezoneConfig = { + /** + * The default timezone to use for the admin panel. + */ + defaultTimezone?: string + /** + * Provide your own list of supported timezones for the admin panel + * + * Values should be IANA timezone names, eg. `America/New_York` + * + * We use `@date-fns/tz` to handle timezones + */ + supportedTimezones?: SupportedTimezonesFn | Timezone[] +} + +type SanitizedTimezoneConfig = { + supportedTimezones: Timezone[] +} & Omit + export type CustomComponent> = PayloadComponent @@ -880,6 +906,10 @@ export type Config = { * @default 'all' // The theme can be configured by users */ theme?: 'all' | 'dark' | 'light' + /** + * Configure timezone related settings for the admin panel. + */ + timezone?: TimezoneConfig /** The slug of a Collection that you want to be used to log in to the Admin dashboard. */ user?: string } @@ -1149,6 +1179,9 @@ export type Config = { } export type SanitizedConfig = { + admin: { + timezone: SanitizedTimezoneConfig + } & DeepRequired collections: SanitizedCollectionConfig[] /** Default richtext editor to use for richText fields */ editor?: RichTextAdapter @@ -1173,7 +1206,7 @@ export type SanitizedConfig = { // E.g. in packages/ui/src/graphics/Account/index.tsx in getComponent, if avatar.Component is casted to what it's supposed to be, // the result type is different DeepRequired, - 'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload' + 'admin' | 'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload' > export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index 2c476adb8a3..7546180a948 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -12,6 +12,8 @@ export { defaults as collectionDefaults } from '../collections/config/defaults.j export { serverProps } from '../config/types.js' +export { defaultTimezones } from '../fields/baseFields/timezone/defaultTimezones.js' + export { fieldAffectsData, fieldHasMaxDepth, diff --git a/packages/payload/src/fields/baseFields/timezone/baseField.ts b/packages/payload/src/fields/baseFields/timezone/baseField.ts new file mode 100644 index 00000000000..f0cd6592bf4 --- /dev/null +++ b/packages/payload/src/fields/baseFields/timezone/baseField.ts @@ -0,0 +1,19 @@ +import type { SelectField } from '../../config/types.js' + +export const baseTimezoneField: (args: Partial) => SelectField = ({ + name, + defaultValue, + options, + required, +}) => { + return { + name, + type: 'select', + admin: { + hidden: true, + }, + defaultValue, + options, + required, + } +} diff --git a/packages/payload/src/fields/baseFields/timezone/defaultTimezones.ts b/packages/payload/src/fields/baseFields/timezone/defaultTimezones.ts new file mode 100644 index 00000000000..59376babd57 --- /dev/null +++ b/packages/payload/src/fields/baseFields/timezone/defaultTimezones.ts @@ -0,0 +1,59 @@ +import type { Timezone } from '../../../config/types.js' + +/** + * List of supported timezones + * + * label: UTC offset and location + * value: IANA timezone name + * + * @example + * { label: '(UTC-12:00) International Date Line West', value: 'Dateline Standard Time' } + */ +export const defaultTimezones: Timezone[] = [ + { label: '(UTC-11:00) Midway Island, Samoa', value: 'Pacific/Midway' }, + { label: '(UTC-11:00) Niue', value: 'Pacific/Niue' }, + { label: '(UTC-10:00) Hawaii', value: 'Pacific/Honolulu' }, + { label: '(UTC-10:00) Cook Islands', value: 'Pacific/Rarotonga' }, + { label: '(UTC-09:00) Alaska', value: 'America/Anchorage' }, + { label: '(UTC-09:00) Gambier Islands', value: 'Pacific/Gambier' }, + { label: '(UTC-08:00) Pacific Time (US & Canada)', value: 'America/Los_Angeles' }, + { label: '(UTC-08:00) Tijuana, Baja California', value: 'America/Tijuana' }, + { label: '(UTC-07:00) Mountain Time (US & Canada)', value: 'America/Denver' }, + { label: '(UTC-07:00) Arizona (No DST)', value: 'America/Phoenix' }, + { label: '(UTC-06:00) Central Time (US & Canada)', value: 'America/Chicago' }, + { label: '(UTC-06:00) Central America', value: 'America/Guatemala' }, + { label: '(UTC-05:00) Eastern Time (US & Canada)', value: 'America/New_York' }, + { label: '(UTC-05:00) Bogota, Lima, Quito', value: 'America/Bogota' }, + { label: '(UTC-04:00) Caracas', value: 'America/Caracas' }, + { label: '(UTC-04:00) Santiago', value: 'America/Santiago' }, + { label: '(UTC-03:00) Buenos Aires', value: 'America/Buenos_Aires' }, + { label: '(UTC-03:00) Brasilia', value: 'America/Sao_Paulo' }, + { label: '(UTC-02:00) South Georgia', value: 'Atlantic/South_Georgia' }, + { label: '(UTC-01:00) Azores', value: 'Atlantic/Azores' }, + { label: '(UTC-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' }, + { label: '(UTC+00:00) London (GMT)', value: 'Europe/London' }, + { label: '(UTC+01:00) Berlin, Paris', value: 'Europe/Berlin' }, + { label: '(UTC+01:00) Lagos', value: 'Africa/Lagos' }, + { label: '(UTC+02:00) Athens, Bucharest', value: 'Europe/Athens' }, + { label: '(UTC+02:00) Cairo', value: 'Africa/Cairo' }, + { label: '(UTC+03:00) Moscow, St. Petersburg', value: 'Europe/Moscow' }, + { label: '(UTC+03:00) Riyadh', value: 'Asia/Riyadh' }, + { label: '(UTC+04:00) Dubai', value: 'Asia/Dubai' }, + { label: '(UTC+04:00) Baku', value: 'Asia/Baku' }, + { label: '(UTC+05:00) Islamabad, Karachi', value: 'Asia/Karachi' }, + { label: '(UTC+05:00) Tashkent', value: 'Asia/Tashkent' }, + { label: '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi', value: 'Asia/Calcutta' }, + { label: '(UTC+06:00) Dhaka', value: 'Asia/Dhaka' }, + { label: '(UTC+06:00) Almaty', value: 'Asia/Almaty' }, + { label: '(UTC+07:00) Jakarta', value: 'Asia/Jakarta' }, + { label: '(UTC+07:00) Bangkok', value: 'Asia/Bangkok' }, + { label: '(UTC+08:00) Beijing, Shanghai', value: 'Asia/Shanghai' }, + { label: '(UTC+08:00) Singapore', value: 'Asia/Singapore' }, + { label: '(UTC+09:00) Tokyo, Osaka, Sapporo', value: 'Asia/Tokyo' }, + { label: '(UTC+09:00) Seoul', value: 'Asia/Seoul' }, + { label: '(UTC+10:00) Sydney, Melbourne', value: 'Australia/Sydney' }, + { label: '(UTC+10:00) Guam, Port Moresby', value: 'Pacific/Guam' }, + { label: '(UTC+11:00) New Caledonia', value: 'Pacific/Noumea' }, + { label: '(UTC+12:00) Auckland, Wellington', value: 'Pacific/Auckland' }, + { label: '(UTC+12:00) Fiji', value: 'Pacific/Fiji' }, +] diff --git a/packages/payload/src/fields/config/sanitize.ts b/packages/payload/src/fields/config/sanitize.ts index 7d10ff7c03c..e1292bbc62b 100644 --- a/packages/payload/src/fields/config/sanitize.ts +++ b/packages/payload/src/fields/config/sanitize.ts @@ -14,6 +14,8 @@ import { import { formatLabels, toWords } from '../../utilities/formatLabels.js' import { baseBlockFields } from '../baseFields/baseBlockFields.js' import { baseIDField } from '../baseFields/baseIDField.js' +import { baseTimezoneField } from '../baseFields/timezone/baseField.js' +import { defaultTimezones } from '../baseFields/timezone/defaultTimezones.js' import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js' import { validations } from '../validations.js' import { sanitizeJoinField } from './sanitizeJoinField.js' @@ -287,6 +289,30 @@ export const sanitizeFields = async ({ } fields[i] = field + + // Insert our field after assignment + if (field.type === 'date' && field.timezone) { + const name = field.name + '_tz' + const defaultTimezone = config.admin.timezone.defaultTimezone + + const supportedTimezones = config.admin.timezone.supportedTimezones + + const options = + typeof supportedTimezones === 'function' + ? supportedTimezones({ defaultTimezones }) + : supportedTimezones + + // Need to set the options here manually so that any database enums are generated correctly + // The UI component will import the options from the config + const timezoneField = baseTimezoneField({ + name, + defaultValue: defaultTimezone, + options, + required: field.required, + }) + + fields.splice(++i, 0, timezoneField) + } } return fields diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 561f8580e2c..d736b4aa7f2 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -671,6 +671,10 @@ export type DateField = { date?: ConditionalDateProps placeholder?: Record | string } & Admin + /** + * Enable timezone selection in the admin interface. + */ + timezone?: true type: 'date' validate?: DateFieldValidation } & Omit @@ -678,7 +682,7 @@ export type DateField = { export type DateFieldClient = { admin?: AdminClient & Pick } & FieldBaseClient & - Pick + Pick export type GroupField = { admin?: { diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index 270e1d9fa87..fa2ba973591 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -377,11 +377,27 @@ export const checkbox: CheckboxFieldValidation = (value, { req: { t }, required export type DateFieldValidation = Validate -export const date: DateFieldValidation = (value, { req: { t }, required }) => { - if (value && !isNaN(Date.parse(value.toString()))) { +export const date: DateFieldValidation = ( + value, + { name, req: { t }, required, siblingData, timezone }, +) => { + const validDate = value && !isNaN(Date.parse(value.toString())) + + // We need to also check for the timezone data based on this field's config + // We cannot do this inside the timezone field validation as it's visually hidden + const hasRequiredTimezone = timezone && required + const selectedTimezone: string = siblingData?.[`${name}_tz`] + // Always resolve to true if the field is not required, as timezone may be optional too then + const validTimezone = hasRequiredTimezone ? Boolean(selectedTimezone) : true + + if (validDate && validTimezone) { return true } + if (validDate && !validTimezone) { + return t('validation:timezoneRequired') + } + if (value) { return t('validation:notValidDate', { value }) } diff --git a/packages/payload/src/utilities/configToJSONSchema.ts b/packages/payload/src/utilities/configToJSONSchema.ts index e2aaa39bbcd..a752e5514f9 100644 --- a/packages/payload/src/utilities/configToJSONSchema.ts +++ b/packages/payload/src/utilities/configToJSONSchema.ts @@ -248,7 +248,7 @@ export function fieldsToJSONSchema( return { properties: Object.fromEntries( - fields.reduce((fieldSchemas, field) => { + fields.reduce((fieldSchemas, field, index) => { const isRequired = fieldAffectsData(field) && fieldIsRequired(field) if (isRequired) { requiredFieldNames.add(field.name) @@ -579,25 +579,37 @@ export function fieldsToJSONSchema( } case 'select': { const optionEnums = buildOptionEnums(field.options) - - if (field.hasMany) { + // We get the previous field to check for a date in the case of a timezone select + // This works because timezone selects are always inserted right after a date with 'timezone: true' + const previousField = fields?.[index - 1] + const isTimezoneField = + previousField?.type === 'date' && previousField.timezone && field.name.includes('_tz') + + // Timezone selects should reference the supportedTimezones definition + if (isTimezoneField) { fieldSchema = { - ...baseFieldSchema, - type: withNullableJSONSchemaType('array', isRequired), - items: { - type: 'string', - }, - } - if (optionEnums?.length) { - ;(fieldSchema.items as JSONSchema4).enum = optionEnums + $ref: `#/definitions/supportedTimezones`, } } else { - fieldSchema = { - ...baseFieldSchema, - type: withNullableJSONSchemaType('string', isRequired), - } - if (optionEnums?.length) { - fieldSchema.enum = optionEnums + if (field.hasMany) { + fieldSchema = { + ...baseFieldSchema, + type: withNullableJSONSchemaType('array', isRequired), + items: { + type: 'string', + }, + } + if (optionEnums?.length) { + ;(fieldSchema.items as JSONSchema4).enum = optionEnums + } + } else { + fieldSchema = { + ...baseFieldSchema, + type: withNullableJSONSchemaType('string', isRequired), + } + if (optionEnums?.length) { + fieldSchema.enum = optionEnums + } } } @@ -956,6 +968,18 @@ export function authCollectionToOperationsJSONSchema( } } +// Generates the JSON Schema for supported timezones +export function timezonesToJSONSchema( + supportedTimezones: SanitizedConfig['admin']['timezone']['supportedTimezones'], +): JSONSchema4 { + return { + description: 'Supported timezones in IANA format.', + enum: supportedTimezones.map((timezone) => + typeof timezone === 'string' ? timezone : timezone.value, + ), + } +} + function generateAuthOperationSchemas(collections: SanitizedCollectionConfig[]): JSONSchema4 { const properties = collections.reduce((acc, collection) => { if (collection.auth) { @@ -1034,6 +1058,8 @@ export function configToJSONSchema( {}, ) + const timezoneDefinitions = timezonesToJSONSchema(config.admin.timezone.supportedTimezones) + const authOperationDefinitions = [...config.collections] .filter(({ auth }) => Boolean(auth)) .reduce( @@ -1057,6 +1083,7 @@ export function configToJSONSchema( let jsonSchema: JSONSchema4 = { additionalProperties: false, definitions: { + supportedTimezones: timezoneDefinitions, ...entityDefinitions, ...Object.fromEntries(interfaceNameDefinitions), ...authOperationDefinitions, diff --git a/packages/translations/src/clientKeys.ts b/packages/translations/src/clientKeys.ts index 49e63c31c62..18bcff60db0 100644 --- a/packages/translations/src/clientKeys.ts +++ b/packages/translations/src/clientKeys.ts @@ -266,6 +266,7 @@ export const clientTranslationKeys = createClientTranslationKeys([ 'general:takeOver', 'general:thisLanguage', 'general:time', + 'general:timezone', 'general:titleDeleted', 'general:true', 'general:upcomingEvents', @@ -341,6 +342,7 @@ export const clientTranslationKeys = createClientTranslationKeys([ 'validation:requiresAtLeast', 'validation:shorterThanMax', 'validation:greaterThanMax', + 'validation:timezoneRequired', 'validation:username', 'version:aboutToPublishSelection', diff --git a/packages/translations/src/languages/ar.ts b/packages/translations/src/languages/ar.ts index 13624f601e0..fe3121596e2 100644 --- a/packages/translations/src/languages/ar.ts +++ b/packages/translations/src/languages/ar.ts @@ -327,6 +327,7 @@ export const arTranslations: DefaultTranslationsObject = { takeOver: 'تولي', thisLanguage: 'العربية', time: 'الوقت', + timezone: 'المنطقة الزمنية', titleDeleted: 'تم حذف {{label}} "{{title}}" بنجاح.', true: 'صحيح', unauthorized: 'غير مصرح به', @@ -417,6 +418,7 @@ export const arTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'هذا الحقل يتطلب عدم تجاوز {{count}} {{label}}.', requiresTwoNumbers: 'هذا الحقل يتطلب رقمين.', shorterThanMax: 'يجب أن تكون هذه القيمة أقصر من الحد الأقصى للطول الذي هو {{maxLength}} أحرف.', + timezoneRequired: 'مطلوب منطقة زمنية.', trueOrFalse: 'يمكن أن يكون هذا الحقل مساويًا فقط للقيمتين صحيح أو خطأ.', username: 'يرجى إدخال اسم مستخدم صالح. يمكن أن يحتوي على أحرف، أرقام، شرطات، فواصل وشرطات سفلية.', diff --git a/packages/translations/src/languages/az.ts b/packages/translations/src/languages/az.ts index 60de39ee573..d407b56f0de 100644 --- a/packages/translations/src/languages/az.ts +++ b/packages/translations/src/languages/az.ts @@ -330,6 +330,7 @@ export const azTranslations: DefaultTranslationsObject = { takeOver: 'Əvvəl', thisLanguage: 'Azərbaycan dili', time: 'Vaxt', + timezone: 'Saat qurşağı', titleDeleted: '{{label}} "{{title}}" uğurla silindi.', true: 'Doğru', unauthorized: 'İcazəsiz', @@ -424,6 +425,7 @@ export const azTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Bu sahə {{count}} {{label}}-dan çox olmamalıdır.', requiresTwoNumbers: 'Bu sahə iki nömrə tələb edir.', shorterThanMax: 'Bu dəyər {{maxLength}} simvoldan qısa olmalıdır.', + timezoneRequired: 'Vaxt zonası tələb olunur.', trueOrFalse: 'Bu sahə yalnız doğru və ya yanlış ola bilər.', username: 'Zəhmət olmasa, etibarlı bir istifadəçi adı daxil edin. Hərflər, rəqəmlər, tire, nöqtə və alt xəttlər ola bilər.', diff --git a/packages/translations/src/languages/bg.ts b/packages/translations/src/languages/bg.ts index 670dd0d3db9..469743bca83 100644 --- a/packages/translations/src/languages/bg.ts +++ b/packages/translations/src/languages/bg.ts @@ -330,6 +330,7 @@ export const bgTranslations: DefaultTranslationsObject = { takeOver: 'Поемане', thisLanguage: 'Български', time: 'Време', + timezone: 'Часова зона', titleDeleted: '{{label}} "{{title}}" успешно изтрит.', true: 'Вярно', unauthorized: 'Неоторизиран', @@ -424,6 +425,7 @@ export const bgTranslations: DefaultTranslationsObject = { requiresTwoNumbers: 'Това поле изисква 2 числа.', shorterThanMax: 'Тази стойност трябва да е по-малка от максималната стойност от {{maxLength}} символа.', + timezoneRequired: 'Изисква се часова зона.', trueOrFalse: 'Това поле може да бъде само "true" или "false".', username: 'Моля, въведете валидно потребителско име. Може да съдържа букви, цифри, тирета, точки и долни черти.', diff --git a/packages/translations/src/languages/ca.ts b/packages/translations/src/languages/ca.ts index d1b32e2c7ec..8bf34fe8eab 100644 --- a/packages/translations/src/languages/ca.ts +++ b/packages/translations/src/languages/ca.ts @@ -331,6 +331,7 @@ export const caTranslations: DefaultTranslationsObject = { takeOver: 'Prendre el control', thisLanguage: 'Catala', time: 'Temps', + timezone: 'Fus horari', titleDeleted: '{{label}} "{{title}}" eliminat correctament.', true: 'Veritat', unauthorized: 'No autoritzat', @@ -425,6 +426,7 @@ export const caTranslations: DefaultTranslationsObject = { requiresTwoNumbers: 'Aquest camp requereix dos números.', shorterThanMax: 'Aquest valor ha de ser més curt que la longitud màxima de {{maxLength}} caràcters.', + timezoneRequired: 'Es requereix una zona horària.', trueOrFalse: 'Aquest camp només pot ser igual a true o false.', username: "Si us plau, introdueix un nom d'usuari vàlid. Pot contenir lletres, números, guions, punts i guions baixos.", diff --git a/packages/translations/src/languages/cs.ts b/packages/translations/src/languages/cs.ts index 2e3365b2830..868f0eb7dba 100644 --- a/packages/translations/src/languages/cs.ts +++ b/packages/translations/src/languages/cs.ts @@ -328,6 +328,7 @@ export const csTranslations: DefaultTranslationsObject = { takeOver: 'Převzít', thisLanguage: 'Čeština', time: 'Čas', + timezone: 'Časové pásmo', titleDeleted: '{{label}} "{{title}}" úspěšně smazáno.', true: 'Pravda', unauthorized: 'Neoprávněný', @@ -420,6 +421,7 @@ export const csTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Toto pole vyžaduje ne více než {{count}} {{label}}.', requiresTwoNumbers: 'Toto pole vyžaduje dvě čísla.', shorterThanMax: 'Tato hodnota musí být kratší než maximální délka {{maxLength}} znaků.', + timezoneRequired: 'Je vyžadováno časové pásmo.', trueOrFalse: 'Toto pole může být rovno pouze true nebo false.', username: 'Prosím, zadejte platné uživatelské jméno. Může obsahovat písmena, čísla, pomlčky, tečky a podtržítka.', diff --git a/packages/translations/src/languages/da.ts b/packages/translations/src/languages/da.ts index 3232f7c3859..e7f492fe8de 100644 --- a/packages/translations/src/languages/da.ts +++ b/packages/translations/src/languages/da.ts @@ -329,6 +329,7 @@ export const daTranslations: DefaultTranslationsObject = { takeOver: 'Overtag', thisLanguage: 'Dansk', time: 'Tid', + timezone: 'Tidszone', titleDeleted: '{{label}} "{{title}}" slettet.', true: 'Sandt', unauthorized: 'Uautoriseret', @@ -421,6 +422,7 @@ export const daTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Dette felt kræver maks {{count}} {{label}}.', requiresTwoNumbers: 'Dette felt kræver to numre.', shorterThanMax: 'Denne værdi skal være kortere end den maksimale længde af {{maxLength}} tegn.', + timezoneRequired: 'En tidszone er nødvendig.', trueOrFalse: 'Denne værdi kan kun være lig med sandt eller falsk.', username: 'Indtast et brugernavn. Kan indeholde bogstaver, tal, bindestreger, punktum og underscores.', diff --git a/packages/translations/src/languages/de.ts b/packages/translations/src/languages/de.ts index ad68d0f6cd6..f8285aa95b0 100644 --- a/packages/translations/src/languages/de.ts +++ b/packages/translations/src/languages/de.ts @@ -334,6 +334,7 @@ export const deTranslations: DefaultTranslationsObject = { takeOver: 'Übernehmen', thisLanguage: 'Deutsch', time: 'Zeit', + timezone: 'Zeitzone', titleDeleted: '{{label}} {{title}} wurde erfolgreich gelöscht.', true: 'Wahr', unauthorized: 'Nicht autorisiert', @@ -428,6 +429,7 @@ export const deTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Dieses Feld kann nicht mehr als {{count}} {{label}} enthalten.', requiresTwoNumbers: 'Dieses Feld muss zwei Nummern enthalten.', shorterThanMax: 'Dieser Wert muss kürzer als die maximale Länge von {{maxLength}} sein.', + timezoneRequired: 'Eine Zeitzone ist erforderlich.', trueOrFalse: 'Dieses Feld kann nur wahr oder falsch sein.', username: 'Bitte geben Sie einen gültigen Benutzernamen ein. Dieser kann Buchstaben, Zahlen, Bindestriche, Punkte und Unterstriche enthalten.', diff --git a/packages/translations/src/languages/en.ts b/packages/translations/src/languages/en.ts index df1a811af9d..44709b4f0ce 100644 --- a/packages/translations/src/languages/en.ts +++ b/packages/translations/src/languages/en.ts @@ -331,6 +331,7 @@ export const enTranslations = { takeOver: 'Take over', thisLanguage: 'English', time: 'Time', + timezone: 'Timezone', titleDeleted: '{{label}} "{{title}}" successfully deleted.', true: 'True', unauthorized: 'Unauthorized', @@ -423,6 +424,7 @@ export const enTranslations = { requiresNoMoreThan: 'This field requires no more than {{count}} {{label}}.', requiresTwoNumbers: 'This field requires two numbers.', shorterThanMax: 'This value must be shorter than the max length of {{maxLength}} characters.', + timezoneRequired: 'A timezone is required.', trueOrFalse: 'This field can only be equal to true or false.', username: 'Please enter a valid username. Can contain letters, numbers, hyphens, periods and underscores.', diff --git a/packages/translations/src/languages/es.ts b/packages/translations/src/languages/es.ts index e09c7edeb63..1afda2b06e9 100644 --- a/packages/translations/src/languages/es.ts +++ b/packages/translations/src/languages/es.ts @@ -335,6 +335,7 @@ export const esTranslations: DefaultTranslationsObject = { takeOver: 'Tomar el control', thisLanguage: 'Español', time: 'Tiempo', + timezone: 'Zona horaria', titleDeleted: '{{label}} {{title}} eliminado correctamente.', true: 'Verdadero', unauthorized: 'No autorizado', @@ -427,6 +428,7 @@ export const esTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Este campo require no más de {{count}} {{label}}', requiresTwoNumbers: 'Este campo requiere dos números.', shorterThanMax: 'Este dato debe ser más corto que el máximo de {{maxLength}} caracteres.', + timezoneRequired: 'Se requiere una zona horaria.', trueOrFalse: 'Este campo solamente puede ser verdadero o falso.', username: 'Por favor, introduzca un nombre de usuario válido. Puede contener letras, números, guiones, puntos y guiones bajos.', diff --git a/packages/translations/src/languages/et.ts b/packages/translations/src/languages/et.ts index 5da5b1615c6..8d274f04d1b 100644 --- a/packages/translations/src/languages/et.ts +++ b/packages/translations/src/languages/et.ts @@ -327,6 +327,7 @@ export const etTranslations: DefaultTranslationsObject = { takeOver: 'Võta üle', thisLanguage: 'Eesti', time: 'Aeg', + timezone: 'Ajavöönd', titleDeleted: '{{label}} "{{title}}" edukalt kustutatud.', true: 'Tõene', unauthorized: 'Volitamata', @@ -418,6 +419,7 @@ export const etTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'See väli ei tohi sisaldada rohkem kui {{count}} {{label}}.', requiresTwoNumbers: 'See väli nõuab kahte numbrit.', shorterThanMax: 'See väärtus peab olema lühem kui maksimaalne pikkus {{maxLength}} tähemärki.', + timezoneRequired: 'Ajavöönd on vajalik.', trueOrFalse: 'See väli saab olla ainult tõene või väär.', username: 'Palun sisesta kehtiv kasutajanimi. Võib sisaldada tähti, numbreid, sidekriipse, punkte ja alakriipse.', diff --git a/packages/translations/src/languages/fa.ts b/packages/translations/src/languages/fa.ts index 12d4e3fb576..13dd43e9d67 100644 --- a/packages/translations/src/languages/fa.ts +++ b/packages/translations/src/languages/fa.ts @@ -328,6 +328,7 @@ export const faTranslations: DefaultTranslationsObject = { takeOver: 'تحویل گرفتن', thisLanguage: 'فارسی', time: 'زمان', + timezone: 'منطقه زمانی', titleDeleted: '{{label}} "{{title}}" با موفقیت پاک شد.', true: 'درست', unauthorized: 'غیرمجاز', @@ -420,6 +421,7 @@ export const faTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'این رشته به بیش از {{count}} {{label}} نیاز دارد.', requiresTwoNumbers: 'این کادر به دو عدد نیاز دارد.', shorterThanMax: 'ورودی باید کمتر از {{maxLength}} واژه باشد.', + timezoneRequired: 'نیاز به منطقه زمانی است.', trueOrFalse: 'این کادر فقط می تواند به صورت true یا false باشد.', username: 'لطفاً یک نام کاربری معتبر وارد کنید. می تواند شامل حروف، اعداد، خط فاصله، نقاط و خط زیر باشد.', diff --git a/packages/translations/src/languages/fr.ts b/packages/translations/src/languages/fr.ts index 12e2b697d57..fb5debc2701 100644 --- a/packages/translations/src/languages/fr.ts +++ b/packages/translations/src/languages/fr.ts @@ -338,6 +338,7 @@ export const frTranslations: DefaultTranslationsObject = { takeOver: 'Prendre en charge', thisLanguage: 'Français', time: 'Temps', + timezone: 'Fuseau horaire', titleDeleted: '{{label}} "{{title}}" supprimé(e) avec succès.', true: 'Vrai', unauthorized: 'Non autorisé', @@ -434,6 +435,7 @@ export const frTranslations: DefaultTranslationsObject = { requiresTwoNumbers: 'Ce champ doit avoir deux chiffres.', shorterThanMax: 'Cette valeur doit être inférieure à la longueur maximale de {{maxLength}} caractères.', + timezoneRequired: 'Un fuseau horaire est requis.', trueOrFalse: 'Ce champ ne peut être égal qu’à vrai ou faux.', username: "Veuillez entrer un nom d'utilisateur valide. Il peut contenir des lettres, des chiffres, des tirets, des points et des tirets bas.", diff --git a/packages/translations/src/languages/he.ts b/packages/translations/src/languages/he.ts index ff48978ae8e..fcb29f52497 100644 --- a/packages/translations/src/languages/he.ts +++ b/packages/translations/src/languages/he.ts @@ -323,6 +323,7 @@ export const heTranslations: DefaultTranslationsObject = { takeOver: 'קח פיקוד', thisLanguage: 'עברית', time: 'זמן', + timezone: 'אזור זמן', titleDeleted: '{{label}} "{{title}}" נמחק בהצלחה.', true: 'True', unauthorized: 'אין הרשאה', @@ -413,6 +414,7 @@ export const heTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'שדה זה דורש לא יותר מ-{{count}} {{label}}.', requiresTwoNumbers: 'שדה זה דורש שני מספרים.', shorterThanMax: 'ערך זה חייב להיות קצר מ-{{maxLength}} תווים.', + timezoneRequired: 'נדרשת אזור זמן.', trueOrFalse: 'שדה זה יכול להיות רק true או false.', username: 'אנא הזן שם משתמש חוקי. יכול להכיל אותיות, מספרים, מקפים, נקודות וקווים תחתונים.', validUploadID: 'שדה זה אינו מזהה העלאה תקני.', diff --git a/packages/translations/src/languages/hr.ts b/packages/translations/src/languages/hr.ts index 8c9a44732ac..09f3219e504 100644 --- a/packages/translations/src/languages/hr.ts +++ b/packages/translations/src/languages/hr.ts @@ -330,6 +330,7 @@ export const hrTranslations: DefaultTranslationsObject = { takeOver: 'Preuzmi', thisLanguage: 'Hrvatski', time: 'Vrijeme', + timezone: 'Vremenska zona', titleDeleted: '{{label}} "{{title}}" uspješno izbrisano.', true: 'Istinito', unauthorized: 'Neovlašteno', @@ -422,6 +423,7 @@ export const hrTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Ovo polje zahtjeva ne više od {{count}} {{label}}.', requiresTwoNumbers: 'Ovo polje zahtjeva dva broja.', shorterThanMax: 'Ova vrijednost mora biti kraća od maksimalne dužine od {{maxLength}} znakova', + timezoneRequired: 'Potrebna je vremenska zona.', trueOrFalse: 'Ovo polje može biti samo točno ili netočno', username: 'Unesite važeće korisničko ime. Može sadržavati slova, brojeve, crtice, točke i donje crte.', diff --git a/packages/translations/src/languages/hu.ts b/packages/translations/src/languages/hu.ts index 28c0e449f63..cbdc5bf5b1c 100644 --- a/packages/translations/src/languages/hu.ts +++ b/packages/translations/src/languages/hu.ts @@ -333,6 +333,7 @@ export const huTranslations: DefaultTranslationsObject = { takeOver: 'Átvétel', thisLanguage: 'Magyar', time: 'Idő', + timezone: 'Időzóna', titleDeleted: '{{label}} "{{title}}" sikeresen törölve.', true: 'Igaz', unauthorized: 'Jogosulatlan', @@ -427,6 +428,7 @@ export const huTranslations: DefaultTranslationsObject = { requiresTwoNumbers: 'Ehhez a mezőhöz két szám szükséges.', shorterThanMax: 'Ennek az értéknek rövidebbnek kell lennie, mint a maximálisan megengedett {{maxLength}} karakter.', + timezoneRequired: 'Időzóna szükséges.', trueOrFalse: 'Ez a mező csak igaz vagy hamis lehet.', username: 'Adjon meg egy érvényes felhasználónevet. Tartalmazhat betűket, számokat, kötőjeleket, pontokat és aláhúzásokat.', diff --git a/packages/translations/src/languages/it.ts b/packages/translations/src/languages/it.ts index c5f47c13b17..198bb9ed8ad 100644 --- a/packages/translations/src/languages/it.ts +++ b/packages/translations/src/languages/it.ts @@ -334,6 +334,7 @@ export const itTranslations: DefaultTranslationsObject = { takeOver: 'Prendi il controllo', thisLanguage: 'Italiano', time: 'Tempo', + timezone: 'Fuso orario', titleDeleted: '{{label}} {{title}} eliminato con successo.', true: 'Vero', unauthorized: 'Non autorizzato', @@ -428,6 +429,7 @@ export const itTranslations: DefaultTranslationsObject = { requiresTwoNumbers: 'Questo campo richiede due numeri.', shorterThanMax: 'Questo valore deve essere inferiore alla lunghezza massima di {{maxLength}} caratteri.', + timezoneRequired: 'È richiesto un fuso orario.', trueOrFalse: "Questo campo può essere solo uguale a 'true' o 'false'.", username: 'Inserisci un nome utente valido. Può contenere lettere, numeri, trattini, punti e underscore.', diff --git a/packages/translations/src/languages/ja.ts b/packages/translations/src/languages/ja.ts index 6b2112088d5..7a0c68e0863 100644 --- a/packages/translations/src/languages/ja.ts +++ b/packages/translations/src/languages/ja.ts @@ -330,6 +330,7 @@ export const jaTranslations: DefaultTranslationsObject = { takeOver: '引き継ぐ', thisLanguage: 'Japanese', time: '時間', + timezone: 'タイムゾーン', titleDeleted: '{{label}} "{{title}}" が削除されました。', true: '真実', unauthorized: '未認証', @@ -421,6 +422,7 @@ export const jaTranslations: DefaultTranslationsObject = { requiresNoMoreThan: '最大で {{count}} {{label}} 以下にする必要があります。', requiresTwoNumbers: '2つの数値が必要です。', shorterThanMax: '{{maxLength}} 文字以下にする必要があります。', + timezoneRequired: 'タイムゾーンが必要です。', trueOrFalse: '"true" または "false" の値にする必要があります。', username: '有効なユーザーネームを入力してください。文字、数字、ハイフン、ピリオド、アンダースコアを使用できます。', diff --git a/packages/translations/src/languages/ko.ts b/packages/translations/src/languages/ko.ts index e982e459e97..f1bdc9fe4c0 100644 --- a/packages/translations/src/languages/ko.ts +++ b/packages/translations/src/languages/ko.ts @@ -328,6 +328,7 @@ export const koTranslations: DefaultTranslationsObject = { takeOver: '인수하기', thisLanguage: '한국어', time: '시간', + timezone: '시간대', titleDeleted: '{{label}} "{{title}}"을(를) 삭제했습니다.', true: '참', unauthorized: '권한 없음', @@ -419,6 +420,7 @@ export const koTranslations: DefaultTranslationsObject = { requiresNoMoreThan: '이 입력란은 최대 {{count}} {{label}} 이하이어야 합니다.', requiresTwoNumbers: '이 입력란은 두 개의 숫자가 필요합니다.', shorterThanMax: '이 값은 최대 길이인 {{maxLength}}자보다 짧아야 합니다.', + timezoneRequired: '시간대가 필요합니다.', trueOrFalse: '이 입력란은 true 또는 false만 가능합니다.', username: '유효한 사용자 이름을 입력해 주세요. 글자, 숫자, 하이픈, 마침표, 및 밑줄을 사용할 수 있습니다.', diff --git a/packages/translations/src/languages/my.ts b/packages/translations/src/languages/my.ts index 37c2e3d5b44..88f8eb43059 100644 --- a/packages/translations/src/languages/my.ts +++ b/packages/translations/src/languages/my.ts @@ -333,6 +333,7 @@ export const myTranslations: DefaultTranslationsObject = { takeOver: 'တာဝန်ယူပါ', thisLanguage: 'မြန်မာစာ', time: 'Masa', + timezone: 'Masa Wilayah', titleDeleted: '{{label}} {{title}} အောင်မြင်စွာ ဖျက်သိမ်းခဲ့သည်။', true: 'အမှန်', unauthorized: 'အခွင့်မရှိပါ။', @@ -430,6 +431,7 @@ export const myTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'ဤအကွက်တွင် {{count}} {{label}} ထက် မပိုရပါ။', requiresTwoNumbers: 'ဤအကွက်သည် နံပါတ်နှစ်ခု လိုအပ်ပါသည်။', shorterThanMax: 'ဤတန်ဖိုးသည် စာလုံး {{maxLength}} လုံး၏ အမြင့်ဆုံးအရှည်ထက် ပိုတိုရပါမည်။', + timezoneRequired: 'အချိန်ဇုန်တစ်ခုလိုအပ်သည်။', trueOrFalse: 'ဤအကွက်သည် တစ်ခုခုဖြစ်ရပါမည်။', username: 'Sila masukkan nama pengguna yang sah. Boleh mengandungi huruf, nombor, tanda hubung, titik dan garis bawah.', diff --git a/packages/translations/src/languages/nb.ts b/packages/translations/src/languages/nb.ts index b56324af05b..ad659501878 100644 --- a/packages/translations/src/languages/nb.ts +++ b/packages/translations/src/languages/nb.ts @@ -331,6 +331,7 @@ export const nbTranslations: DefaultTranslationsObject = { takeOver: 'Ta over', thisLanguage: 'Norsk', time: 'Tid', + timezone: 'Tidssone', titleDeleted: '{{label}} "{{title}}" ble slettet.', true: 'Sann', unauthorized: 'Ikke autorisert', @@ -423,6 +424,7 @@ export const nbTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Dette feltet krever maksimalt {{count}} {{label}}.', requiresTwoNumbers: 'Dette feltet krever to tall.', shorterThanMax: 'Denne verdien må være kortere enn maksimal lengde på {{maxLength}} tegn.', + timezoneRequired: 'En tidssone er nødvendig.', trueOrFalse: 'Dette feltet kan bare være likt true eller false.', username: 'Vennligst oppgi et gyldig brukernavn. Kan inneholde bokstaver, nummer, bindestreker, punktum og understrek.', diff --git a/packages/translations/src/languages/nl.ts b/packages/translations/src/languages/nl.ts index de050ec9258..d7c1479b916 100644 --- a/packages/translations/src/languages/nl.ts +++ b/packages/translations/src/languages/nl.ts @@ -334,6 +334,7 @@ export const nlTranslations: DefaultTranslationsObject = { takeOver: 'Overnemen', thisLanguage: 'Nederlands', time: 'Tijd', + timezone: 'Tijdzone', titleDeleted: '{{label}} "{{title}}" succesvol verwijderd.', true: 'Waar', unauthorized: 'Onbevoegd', @@ -426,6 +427,7 @@ export const nlTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Dit veld vereist niet meer dan {{count}} {{label}}.', requiresTwoNumbers: 'Dit veld vereist twee nummers.', shorterThanMax: 'Dit veld moet korter zijn dan de maximale lengte van {{maxLength}} tekens.', + timezoneRequired: 'Een tijdzone is vereist.', trueOrFalse: 'Dit veld kan alleen waar of onwaar zijn.', username: 'Voer een geldige gebruikersnaam in. Kan letters, cijfers, koppeltekens, punten en underscores bevatten.', diff --git a/packages/translations/src/languages/pl.ts b/packages/translations/src/languages/pl.ts index d3276430d5a..2ada974a70f 100644 --- a/packages/translations/src/languages/pl.ts +++ b/packages/translations/src/languages/pl.ts @@ -330,6 +330,7 @@ export const plTranslations: DefaultTranslationsObject = { takeOver: 'Przejąć', thisLanguage: 'Polski', time: 'Czas', + timezone: 'Strefa czasowa', titleDeleted: 'Pomyślnie usunięto {{label}} {{title}}', true: 'Prawda', unauthorized: 'Brak autoryzacji', @@ -422,6 +423,7 @@ export const plTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'To pole może posiadać co najmniej {{count}} {{label}}.', requiresTwoNumbers: 'To pole wymaga dwóch liczb.', shorterThanMax: 'Ta wartość musi być krótsza niż maksymalna długość znaków: {{maxLength}}.', + timezoneRequired: 'Wymagana jest strefa czasowa.', trueOrFalse: "To pole może mieć wartość tylko 'true' lub 'false'.", username: 'Proszę wprowadzić prawidłową nazwę użytkownika. Może zawierać litery, cyfry, myślniki, kropki i podkreślniki.', diff --git a/packages/translations/src/languages/pt.ts b/packages/translations/src/languages/pt.ts index 512065abb1a..ccc089447c6 100644 --- a/packages/translations/src/languages/pt.ts +++ b/packages/translations/src/languages/pt.ts @@ -331,6 +331,7 @@ export const ptTranslations: DefaultTranslationsObject = { takeOver: 'Assumir', thisLanguage: 'Português', time: 'Tempo', + timezone: 'Fuso horário', titleDeleted: '{{label}} {{title}} excluído com sucesso.', true: 'Verdadeiro', unauthorized: 'Não autorizado', @@ -423,6 +424,7 @@ export const ptTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Esse campo requer pelo menos {{count}} {{label}}.', requiresTwoNumbers: 'Esse campo requer dois números.', shorterThanMax: 'Esse valor deve ser menor do que o máximo de {{maxLength}} caracteres.', + timezoneRequired: 'É necessário um fuso horário.', trueOrFalse: 'Esse campo pode ser apenas verdadeiro (true) ou falso (false)', username: 'Por favor, insira um nome de usuário válido. Pode conter letras, números, hifens, pontos e sublinhados.', diff --git a/packages/translations/src/languages/ro.ts b/packages/translations/src/languages/ro.ts index d41edee33ef..f3602c59d5f 100644 --- a/packages/translations/src/languages/ro.ts +++ b/packages/translations/src/languages/ro.ts @@ -334,6 +334,7 @@ export const roTranslations: DefaultTranslationsObject = { takeOver: 'Preia controlul', thisLanguage: 'Română', time: 'Timp', + timezone: 'Fus orar', titleDeleted: '{{label}} "{{title}}" șters cu succes.', true: 'Adevărat', unauthorized: 'neautorizat(ă)', @@ -430,6 +431,7 @@ export const roTranslations: DefaultTranslationsObject = { requiresTwoNumbers: 'Acest câmp necesită două numere.', shorterThanMax: 'Această valoare trebuie să fie mai scurtă decât lungimea maximă de {{maxLength}} caractere.', + timezoneRequired: 'Este necesar un fus orar.', trueOrFalse: 'Acest câmp poate fi doar egal cu true sau false.', username: 'Vă rugăm să introduceți un nume de utilizator valid. Poate conține litere, numere, cratime, puncte și sublinieri.', diff --git a/packages/translations/src/languages/rs.ts b/packages/translations/src/languages/rs.ts index f17a516c443..1204731a4f2 100644 --- a/packages/translations/src/languages/rs.ts +++ b/packages/translations/src/languages/rs.ts @@ -330,6 +330,7 @@ export const rsTranslations: DefaultTranslationsObject = { takeOver: 'Превузети', thisLanguage: 'Српски (ћирилица)', time: 'Vreme', + timezone: 'Vremenska zona', titleDeleted: '{{label}} "{{title}}" успешно обрисано.', true: 'Istinito', unauthorized: 'Нисте ауторизовани', @@ -422,6 +423,7 @@ export const rsTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Ово поље захтева не више од {{count}} {{label}}.', requiresTwoNumbers: 'Ово поље захтева два броја.', shorterThanMax: 'Ова вредност мора бити краћа од максималне дужине од {{maxLength}} карактера', + timezoneRequired: 'Потребна је временска зона.', trueOrFalse: 'Ово поље може бити само тачно или нетачно', username: 'Molimo unesite važeće korisničko ime. Može sadržati slova, brojeve, crtice, tačke i donje crte.', diff --git a/packages/translations/src/languages/rsLatin.ts b/packages/translations/src/languages/rsLatin.ts index c9eb87ca508..6681340f306 100644 --- a/packages/translations/src/languages/rsLatin.ts +++ b/packages/translations/src/languages/rsLatin.ts @@ -331,6 +331,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = { takeOver: 'Preuzeti', thisLanguage: 'Srpski (latinica)', time: 'Vreme', + timezone: 'Vremenska zona', titleDeleted: '{{label}} "{{title}}" uspešno obrisano.', true: 'Istinito', unauthorized: 'Niste autorizovani', @@ -423,6 +424,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Ovo polje zahteva ne više od {{count}} {{label}}.', requiresTwoNumbers: 'Ovo polje zahteva dva broja.', shorterThanMax: 'Ova vrednost mora biti kraća od maksimalne dužine od {{maxLength}} karaktera', + timezoneRequired: 'Potrebna je vremenska zona.', trueOrFalse: 'Ovo polje može biti samo tačno ili netačno', username: 'Molimo unesite važeće korisničko ime. Može sadržavati slova, brojeve, crtice, tačke i donje crte.', diff --git a/packages/translations/src/languages/ru.ts b/packages/translations/src/languages/ru.ts index 1dabacbc128..2296b1276f1 100644 --- a/packages/translations/src/languages/ru.ts +++ b/packages/translations/src/languages/ru.ts @@ -332,6 +332,7 @@ export const ruTranslations: DefaultTranslationsObject = { takeOver: 'Взять на себя', thisLanguage: 'Русский', time: 'Время', + timezone: 'Часовой пояс', titleDeleted: '{{label}} {{title}} успешно удалено.', true: 'Правда', unauthorized: 'Нет доступа', @@ -426,6 +427,7 @@ export const ruTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Это поле требует не более {{count}} {{label}}', requiresTwoNumbers: 'В этом поле требуется два числа.', shorterThanMax: 'Это значение должно быть короче максимальной длины символов {{maxLength}}.', + timezoneRequired: 'Требуется указать часовой пояс.', trueOrFalse: 'Это поле может быть равно только true или false.', username: 'Пожалуйста, введите действительное имя пользователя. Может содержать буквы, цифры, дефисы, точки и подчёркивания.', diff --git a/packages/translations/src/languages/sk.ts b/packages/translations/src/languages/sk.ts index c1349f95b31..e1e732d40c7 100644 --- a/packages/translations/src/languages/sk.ts +++ b/packages/translations/src/languages/sk.ts @@ -331,6 +331,7 @@ export const skTranslations: DefaultTranslationsObject = { takeOver: 'Prevziať', thisLanguage: 'Slovenčina', time: 'Čas', + timezone: 'Časové pásmo', titleDeleted: '{{label}} "{{title}}" úspešne zmazané.', true: 'Pravda', unauthorized: 'Neoprávnený prístup', @@ -423,6 +424,7 @@ export const skTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Toto pole vyžaduje nie viac ako {{count}} {{label}}.', requiresTwoNumbers: 'Toto pole vyžaduje dve čísla.', shorterThanMax: 'Táto hodnota musí byť kratšia ako maximálna dĺžka {{maxLength}} znakov.', + timezoneRequired: 'Je potrebné uviesť časové pásmo.', trueOrFalse: 'Toto pole môže byť rovné iba true alebo false.', username: 'Prosím, zadajte platné používateľské meno. Môže obsahovať písmená, čísla, pomlčky, bodky a podčiarknutia.', diff --git a/packages/translations/src/languages/sl.ts b/packages/translations/src/languages/sl.ts index a0c7ecd6cc0..0169d76a225 100644 --- a/packages/translations/src/languages/sl.ts +++ b/packages/translations/src/languages/sl.ts @@ -329,6 +329,7 @@ export const slTranslations: DefaultTranslationsObject = { takeOver: 'Prevzemi', thisLanguage: 'Slovenščina', time: 'Čas', + timezone: 'Časovni pas', titleDeleted: '{{label}} "{{title}}" uspešno izbrisan.', true: 'Da', unauthorized: 'Nepooblaščeno', @@ -421,6 +422,7 @@ export const slTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'To polje zahteva največ {{count}} {{label}}.', requiresTwoNumbers: 'To polje zahteva dve številki.', shorterThanMax: 'Ta vrednost mora biti krajša od največje dolžine {{maxLength}} znakov.', + timezoneRequired: 'Potrebna je časovna cona.', trueOrFalse: 'To polje je lahko samo enako true ali false.', username: 'Vnesite veljavno uporabniško ime. Lahko vsebuje črke, številke, vezaje, pike in podčrtaje.', diff --git a/packages/translations/src/languages/sv.ts b/packages/translations/src/languages/sv.ts index 5e477117944..95d21aeef47 100644 --- a/packages/translations/src/languages/sv.ts +++ b/packages/translations/src/languages/sv.ts @@ -331,6 +331,7 @@ export const svTranslations: DefaultTranslationsObject = { takeOver: 'Ta över', thisLanguage: 'Svenska', time: 'Tid', + timezone: 'Tidszon', titleDeleted: '{{label}} "{{title}}" togs bort framgångsrikt.', true: 'Sann', unauthorized: 'Obehörig', @@ -423,6 +424,7 @@ export const svTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Detta fält kräver inte mer än {{count}} {{label}}.', requiresTwoNumbers: 'Detta fält kräver två nummer.', shorterThanMax: 'Detta värde måste vara kortare än maxlängden på {{maxLength}} tecken.', + timezoneRequired: 'En tidszon krävs.', trueOrFalse: 'Detta fält kan bara vara lika med sant eller falskt.', username: 'Var god ange ett giltigt användarnamn. Kan innehålla bokstäver, siffror, bindestreck, punkter och understreck.', diff --git a/packages/translations/src/languages/th.ts b/packages/translations/src/languages/th.ts index 43d703e7004..6c04fa651d3 100644 --- a/packages/translations/src/languages/th.ts +++ b/packages/translations/src/languages/th.ts @@ -326,6 +326,7 @@ export const thTranslations: DefaultTranslationsObject = { takeOver: 'เข้ายึด', thisLanguage: 'ไทย', time: 'เวลา', + timezone: 'เขตเวลา', titleDeleted: 'ลบ {{label}} "{{title}}" สำเร็จ', true: 'จริง', unauthorized: 'ไม่ได้รับอนุญาต', @@ -416,6 +417,7 @@ export const thTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'ห้ามมีเกิน {{count}} {{label}}', requiresTwoNumbers: 'ต้องมีตัวเลข 2 ค่า', shorterThanMax: 'ค่าต้องมีความยาวน้อยกว่า {{maxLength}} ตัวอักษร', + timezoneRequired: 'ต้องการเขตเวลา', trueOrFalse: 'เป็นได้แค่ "ใช่" หรือ "ไม่ใช่"', username: 'กรุณาใส่ชื่อผู้ใช้ที่ถูกต้อง สามารถมีตัวอักษร ตัวเลข ขีดกลาง จุด และขีดล่าง', validUploadID: 'ไม่ใช่ ID ของการอัปโหลดที่ถูกต้อง', diff --git a/packages/translations/src/languages/tr.ts b/packages/translations/src/languages/tr.ts index 969dfa0a28b..d0289f3fd18 100644 --- a/packages/translations/src/languages/tr.ts +++ b/packages/translations/src/languages/tr.ts @@ -334,6 +334,7 @@ export const trTranslations: DefaultTranslationsObject = { takeOver: 'Devralmak', thisLanguage: 'Türkçe', time: 'Zaman', + timezone: 'Saat dilimi', titleDeleted: '{{label}} {{title}} başarıyla silindi.', true: 'Doğru', unauthorized: 'Yetkisiz', @@ -427,6 +428,7 @@ export const trTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Bu alana {{count}} adetten fazla {{label}} girilemez.', requiresTwoNumbers: 'Bu alana en az iki rakam girilmesi zorunludur.', shorterThanMax: 'Bu alan {{maxLength}} karakterden daha kısa olmalıdır.', + timezoneRequired: 'Bir zaman dilimi gereklidir.', trueOrFalse: 'Bu alan yalnızca doğru ve yanlış olabilir.', username: 'Lütfen geçerli bir kullanıcı adı girin. Harfler, numaralar, kısa çizgiler, noktalar ve alt çizgiler içerebilir.', diff --git a/packages/translations/src/languages/uk.ts b/packages/translations/src/languages/uk.ts index f6a00894bba..f2e544feda4 100644 --- a/packages/translations/src/languages/uk.ts +++ b/packages/translations/src/languages/uk.ts @@ -329,6 +329,7 @@ export const ukTranslations: DefaultTranslationsObject = { takeOver: 'Перейняти', thisLanguage: 'Українська', time: 'Час', + timezone: 'Часовий пояс', titleDeleted: '{{label}} "{{title}}" успішно видалено.', true: 'Правда', unauthorized: 'Немає доступу', @@ -421,6 +422,7 @@ export const ukTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Це поле потребує не більше {{count}} {{label}}.', requiresTwoNumbers: 'У цьому полі потрібно ввести два числа.', shorterThanMax: 'Це значення має дорівнювати або бути коротшим, ніж {{maxLength}} символів.', + timezoneRequired: 'Потрібний часовий пояс.', trueOrFalse: 'Це поле може мати значення тільки true або false.', username: "Будь ласка, введіть дійсне ім'я користувача. Може містити літери, цифри, дефіси, крапки та підкреслення.", diff --git a/packages/translations/src/languages/vi.ts b/packages/translations/src/languages/vi.ts index 1dc95042e85..a3b6eb7f25c 100644 --- a/packages/translations/src/languages/vi.ts +++ b/packages/translations/src/languages/vi.ts @@ -329,6 +329,7 @@ export const viTranslations: DefaultTranslationsObject = { takeOver: 'Tiếp quản', thisLanguage: 'Vietnamese (Tiếng Việt)', time: 'Thời gian', + timezone: 'Múi giờ', titleDeleted: '{{label}} {{title}} đã được xóa thành công.', true: 'Thật', unauthorized: 'Không có quyền truy cập.', @@ -421,6 +422,7 @@ export const viTranslations: DefaultTranslationsObject = { requiresNoMoreThan: 'Field này không thể vượt quá {{count}} {{label}}.', requiresTwoNumbers: 'Field này cần tối thiểu 2 chữ số.', shorterThanMax: 'Giá trị phải ngắn hơn hoặc bằng {{maxLength}} ký tự.', + timezoneRequired: 'Yêu cầu phải có múi giờ.', trueOrFalse: 'Field này chỉ có thể chứa giá trị true hoặc false.', username: 'Vui lòng nhập một tên người dùng hợp lệ. Có thể chứa các chữ cái, số, dấu gạch ngang, dấu chấm và dấu gạch dưới.', diff --git a/packages/translations/src/languages/zh.ts b/packages/translations/src/languages/zh.ts index ef3ef7f4d91..74e3b242651 100644 --- a/packages/translations/src/languages/zh.ts +++ b/packages/translations/src/languages/zh.ts @@ -319,6 +319,7 @@ export const zhTranslations: DefaultTranslationsObject = { takeOver: '接管', thisLanguage: '中文 (简体)', time: '时间', + timezone: '时区', titleDeleted: '{{label}} "{{title}}"已被成功删除。', true: '真实', unauthorized: '未经授权', @@ -409,6 +410,7 @@ export const zhTranslations: DefaultTranslationsObject = { requiresNoMoreThan: '该字段要求不超过{{count}} {{label}。', requiresTwoNumbers: '该字段需要两个数字。', shorterThanMax: '该值必须小于{{maxLength}}字符的最大长度', + timezoneRequired: '需要一个时区。', trueOrFalse: '该字段只能等于真或伪。', username: '请输入一个有效的用户名。可包含字母,数字,连字符,句点和下划线。', validUploadID: '该字段不是有效的上传ID。', diff --git a/packages/translations/src/languages/zhTw.ts b/packages/translations/src/languages/zhTw.ts index 7dedd1443af..bf946252913 100644 --- a/packages/translations/src/languages/zhTw.ts +++ b/packages/translations/src/languages/zhTw.ts @@ -319,6 +319,7 @@ export const zhTwTranslations: DefaultTranslationsObject = { takeOver: '接管', thisLanguage: '中文 (繁體)', time: '時間', + timezone: '時區', titleDeleted: '{{label}} "{{title}}"已被成功刪除。', true: '真實', unauthorized: '未經授權', @@ -409,6 +410,7 @@ export const zhTwTranslations: DefaultTranslationsObject = { requiresNoMoreThan: '該字串要求不超過 {{count}} 個 {{label}。', requiresTwoNumbers: '該字串需要兩個數字。', shorterThanMax: '該值長度必須小於{{maxLength}}個字元', + timezoneRequired: '需要時間區。', trueOrFalse: '該字串只能等於是或否。', username: '請輸入有效的使用者名稱。可以包含字母、數字、連字號、句點和底線。', validUploadID: '該字串不是有效的上傳ID。', diff --git a/packages/ui/package.json b/packages/ui/package.json index 7e1e3453175..8578eda36eb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -116,6 +116,7 @@ "prepublishOnly": "pnpm clean && pnpm turbo build" }, "dependencies": { + "@date-fns/tz": "1.2.0", "@dnd-kit/core": "6.0.8", "@dnd-kit/sortable": "7.0.2", "@faceless-ui/modal": "3.0.0-beta.2", diff --git a/packages/ui/src/elements/DatePicker/types.ts b/packages/ui/src/elements/DatePicker/types.ts index ab162540082..c5bb92a3c9a 100644 --- a/packages/ui/src/elements/DatePicker/types.ts +++ b/packages/ui/src/elements/DatePicker/types.ts @@ -4,7 +4,7 @@ export type Props = { onChange?: (val: Date) => void placeholder?: string readOnly?: boolean - value?: Date + value?: Date | string } & DayPickerProps & SharedProps & TimePickerProps diff --git a/packages/ui/src/elements/TimezonePicker/index.scss b/packages/ui/src/elements/TimezonePicker/index.scss new file mode 100644 index 00000000000..365bf814fa3 --- /dev/null +++ b/packages/ui/src/elements/TimezonePicker/index.scss @@ -0,0 +1,51 @@ +@layer payload-default { + .timezone-picker-wrapper { + display: flex; + gap: calc(var(--base) / 4); + margin-top: calc(var(--base) / 4); + align-items: center; + + .field-label { + margin-right: unset; + color: var(--theme-elevation-400); + flex-shrink: 0; + } + + .timezone-picker { + display: inline-block; + + .rs__menu { + min-width: calc(var(--base) * 14); + overflow: hidden; + border-radius: calc(var(--base) * 0.25); + } + + .rs__value-container { + text-align: center; + } + + .rs__control { + background: none; + border: none; + padding: 0; + min-height: auto !important; + position: relative; + box-shadow: unset; + min-width: var(--base); + + &:hover { + cursor: pointer; + box-shadow: unset; + } + + &.rs__control--menu-is-open::before { + display: block; + } + } + + .rs__indicators { + margin-inline-start: calc(var(--base) * 0.25); + } + } + } +} diff --git a/packages/ui/src/elements/TimezonePicker/index.tsx b/packages/ui/src/elements/TimezonePicker/index.tsx new file mode 100644 index 00000000000..c41d84b6ade --- /dev/null +++ b/packages/ui/src/elements/TimezonePicker/index.tsx @@ -0,0 +1,59 @@ +'use client' + +import type { OptionObject } from 'payload' +import type React from 'react' + +import { useMemo } from 'react' + +import type { Props } from './types.js' + +import { FieldLabel } from '../../fields/FieldLabel/index.js' +import './index.scss' +import { useTranslation } from '../../providers/Translation/index.js' +import { ReactSelect } from '../ReactSelect/index.js' +import { formatOptions } from '../WhereBuilder/Condition/Select/formatOptions.js' + +export const TimezonePicker: React.FC = (props) => { + const { + id, + onChange: onChangeFromProps, + options: optionsFromProps, + required, + selectedTimezone: selectedTimezoneFromProps, + } = props + + const { t } = useTranslation() + + const options = formatOptions(optionsFromProps) + + const selectedTimezone = useMemo(() => { + return options.find((t) => { + const value = typeof t === 'string' ? t : t.value + return value === (selectedTimezoneFromProps || 'UTC') + }) + }, [options, selectedTimezoneFromProps]) + + return ( +
+ + { + if (onChangeFromProps) { + onChangeFromProps(val?.value || '') + } + }} + options={options} + value={selectedTimezone} + /> +
+ ) +} diff --git a/packages/ui/src/elements/TimezonePicker/types.ts b/packages/ui/src/elements/TimezonePicker/types.ts new file mode 100644 index 00000000000..fdbb0100b55 --- /dev/null +++ b/packages/ui/src/elements/TimezonePicker/types.ts @@ -0,0 +1,8 @@ +import type { SelectFieldClient } from 'payload' + +export type Props = { + id: string + onChange?: (val: string) => void + required?: boolean + selectedTimezone?: string +} & Pick diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Select/formatOptions.ts b/packages/ui/src/elements/WhereBuilder/Condition/Select/formatOptions.ts new file mode 100644 index 00000000000..bbeea8ada00 --- /dev/null +++ b/packages/ui/src/elements/WhereBuilder/Condition/Select/formatOptions.ts @@ -0,0 +1,16 @@ +import type { Option, OptionObject } from 'payload' + +/** + * Formats an array of options for use in a select input. + */ +export const formatOptions = (options: Option[]): OptionObject[] => + options.map((option) => { + if (typeof option === 'object' && (option.value || option.value === '')) { + return option + } + + return { + label: option, + value: option, + } as OptionObject + }) diff --git a/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx b/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx index fa1035fcbe6..72cdce61df0 100644 --- a/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx +++ b/packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx @@ -1,5 +1,4 @@ 'use client' -import type { Option, OptionObject } from 'payload' import { getTranslation } from '@payloadcms/translations' import React from 'react' @@ -8,18 +7,7 @@ import type { Props } from './types.js' import { useTranslation } from '../../../../providers/Translation/index.js' import { ReactSelect } from '../../../ReactSelect/index.js' - -const formatOptions = (options: Option[]): OptionObject[] => - options.map((option) => { - if (typeof option === 'object' && (option.value || option.value === '')) { - return option - } - - return { - label: option, - value: option, - } as OptionObject - }) +import { formatOptions } from './formatOptions.js' export const Select: React.FC = ({ disabled, diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index e561ae828d3..1983f2d7a87 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -125,6 +125,7 @@ export { FileDetails } from '../../elements/FileDetails/index.js' export { PreviewSizes } from '../../elements/PreviewSizes/index.js' export { PreviewButton } from '../../elements/PreviewButton/index.js' export { RelationshipTable } from '../../elements/RelationshipTable/index.js' +export { TimezonePicker } from '../../elements/TimezonePicker/index.js' export { BlocksDrawer } from '../../fields/Blocks/BlocksDrawer/index.js' export { SectionTitle } from '../../fields/Blocks/SectionTitle/index.js' diff --git a/packages/ui/src/fields/DateTime/index.tsx b/packages/ui/src/fields/DateTime/index.tsx index 3c751855954..2bf092a5e6d 100644 --- a/packages/ui/src/fields/DateTime/index.tsx +++ b/packages/ui/src/fields/DateTime/index.tsx @@ -1,19 +1,24 @@ 'use client' import type { DateFieldClientComponent, DateFieldValidation } from 'payload' +import { TZDateMini as TZDate } from '@date-fns/tz/date/mini' import { getTranslation } from '@payloadcms/translations' -import React, { useCallback, useMemo } from 'react' +import { transpose } from 'date-fns' +import { useCallback, useMemo } from 'react' import { DatePickerField } from '../../elements/DatePicker/index.js' import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js' +import { TimezonePicker } from '../../elements/TimezonePicker/index.js' import { FieldDescription } from '../../fields/FieldDescription/index.js' import { FieldError } from '../../fields/FieldError/index.js' import { FieldLabel } from '../../fields/FieldLabel/index.js' +import { useForm, useFormFields } from '../../forms/Form/context.js' import { useField } from '../../forms/useField/index.js' +import './index.scss' import { withCondition } from '../../forms/withCondition/index.js' +import { useConfig } from '../../providers/Config/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { mergeFieldStyles } from '../mergeFieldStyles.js' -import './index.scss' import { fieldBaseClass } from '../shared/index.js' const baseClass = 'date-time-field' @@ -26,13 +31,19 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => { label, localized, required, + timezone, }, path, readOnly, validate, } = props + // Get the user timezone so we can adjust the displayed value against it + const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone + + const { config } = useConfig() const { i18n } = useTranslation() + const { dispatchFields, setModified } = useForm() const memoizedValidate: DateFieldValidation = useCallback( (value, options) => { @@ -53,8 +64,73 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => { validate: memoizedValidate, }) + const timezonePath = path + '_tz' + const timezoneField = useFormFields(([fields, _]) => fields?.[timezonePath]) + const supportedTimezones = config.admin.timezone.supportedTimezones + + const selectedTimezone = timezoneField?.value as string + + // The displayed value should be the original value, adjusted to the user's timezone + const displayedValue = useMemo(() => { + if (timezone && selectedTimezone && userTimezone && value) { + // Create TZDate instances for the selected timezone and the user's timezone + // These instances allow us to transpose the date between timezones while keeping the same time value + const DateWithOriginalTz = TZDate.tz(selectedTimezone) + const DateWithUserTz = TZDate.tz(userTimezone) + + const modifiedDate = new TZDate(value).withTimeZone(selectedTimezone) + + // Transpose the date to the selected timezone + const dateWithTimezone = transpose(modifiedDate, DateWithOriginalTz) + + // Transpose the date to the user's timezone - this is necessary because the react-datepicker component insists on displaying the date in the user's timezone + const dateWithUserTimezone = transpose(dateWithTimezone, DateWithUserTz) + + return dateWithUserTimezone.toISOString() + } + + return value + }, [timezone, selectedTimezone, value, userTimezone]) + const styles = useMemo(() => mergeFieldStyles(field), [field]) + const onChange = useCallback( + (incomingDate: Date) => { + if (!readOnly) { + if (timezone && selectedTimezone && incomingDate) { + // Create TZDate instances for the selected timezone + const tzDateWithUTC = TZDate.tz(selectedTimezone) + + // Creates a TZDate instance for the user's timezone — this is default behaviour of TZDate as it wraps the Date constructor + const dateToUserTz = new TZDate(incomingDate) + + // Transpose the date to the selected timezone + const dateWithTimezone = transpose(dateToUserTz, tzDateWithUTC) + + setValue(dateWithTimezone.toISOString() || null) + } else { + setValue(incomingDate?.toISOString() || null) + } + } + }, + [readOnly, setValue, timezone, selectedTimezone], + ) + + const onChangeTimezone = useCallback( + (timezone: string) => { + if (timezonePath) { + dispatchFields({ + type: 'UPDATE', + path: timezonePath, + value: timezone, + }) + + setModified(true) + } + }, + [dispatchFields, setModified, timezonePath], + ) + return (
{ {BeforeInput} { - if (!readOnly) { - setValue(incomingDate?.toISOString() || null) - } + onChange={onChange} + overrides={{ + ...datePickerProps?.overrides, }} placeholder={getTranslation(placeholder, i18n)} readOnly={readOnly} - value={value} + value={displayedValue} /> + {timezone && supportedTimezones.length > 0 && ( + + )} + {AfterInput}
=10.0.0'} @@ -11698,6 +11707,8 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@date-fns/tz@1.2.0': {} + '@discoveryjs/json-ext@0.5.7': {} '@dnd-kit/accessibility@3.1.0(react@19.0.0)': diff --git a/test/fields/collections/Date/e2e.spec.ts b/test/fields/collections/Date/e2e.spec.ts index 97ce12ba55a..4e85e349c2a 100644 --- a/test/fields/collections/Date/e2e.spec.ts +++ b/test/fields/collections/Date/e2e.spec.ts @@ -1,7 +1,9 @@ import type { Page } from '@playwright/test' +import { TZDateMini } from '@date-fns/tz/date/mini' import { expect, test } from '@playwright/test' import path from 'path' +import { wait } from 'payload/shared' import { fileURLToPath } from 'url' import type { PayloadTestSDK } from '../../../helpers/sdk/index.js' @@ -25,6 +27,8 @@ const dirname = path.resolve(currentFolder, '../../') const { beforeAll, beforeEach, describe } = test +const londonTimezone = 'Europe/London' + let payload: PayloadTestSDK let client: RESTClient let page: Page @@ -85,6 +89,18 @@ describe('Date', () => { await expect(dateField).toBeVisible() await dateField.fill('02/07/2023') await expect(dateField).toHaveValue('02/07/2023') + + // Fill in remaining required fields, this is just to make sure saving is possible + const dateWithTz = page.locator('#field-dayAndTimeWithTimezone .react-datepicker-wrapper input') + + await dateWithTz.fill('08/12/2027 10:00 AM') + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("London")` + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await saveDocAndAssert(page) const clearButton = page.locator('#field-default .date-time-picker__clear-button') @@ -109,6 +125,20 @@ describe('Date', () => { // enter date in default date field await dateField.fill('02/07/2023') + + // Fill in remaining required fields, this is just to make sure saving is possible + const dateWithTz = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + await dateWithTz.fill('08/12/2027 10:00 AM') + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("London")` + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await saveDocAndAssert(page) // get the ID of the doc @@ -116,9 +146,11 @@ describe('Date', () => { const id = routeSegments.pop() // fetch the doc (need the date string from the DB) - const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' }) + const { doc } = await client.findByID({ id: id!, auth: true, slug: 'date-fields' }) - expect(doc.default).toEqual('2023-02-07T12:00:00.000Z') + await expect(() => { + expect(doc.default).toEqual('2023-02-07T12:00:00.000Z') + }).toPass({ timeout: 10000, intervals: [100] }) }) }) @@ -138,6 +170,20 @@ describe('Date', () => { // enter date in default date field await dateField.fill('02/07/2023') + + // Fill in remaining required fields, this is just to make sure saving is possible + const dateWithTz = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + await dateWithTz.fill('08/12/2027 10:00 AM') + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("London")` + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await saveDocAndAssert(page) // get the ID of the doc @@ -145,9 +191,11 @@ describe('Date', () => { const id = routeSegments.pop() // fetch the doc (need the date string from the DB) - const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' }) + const { doc } = await client.findByID({ id: id!, auth: true, slug: 'date-fields' }) - expect(doc.default).toEqual('2023-02-07T12:00:00.000Z') + await expect(() => { + expect(doc.default).toEqual('2023-02-07T12:00:00.000Z') + }).toPass({ timeout: 10000, intervals: [100] }) }) }) @@ -167,6 +215,20 @@ describe('Date', () => { // enter date in default date field await dateField.fill('02/07/2023') + + // Fill in remaining required fields, this is just to make sure saving is possible + const dateWithTz = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + await dateWithTz.fill('08/12/2027 10:00 AM') + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("London")` + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await saveDocAndAssert(page) // get the ID of the doc @@ -174,9 +236,384 @@ describe('Date', () => { const id = routeSegments.pop() // fetch the doc (need the date string from the DB) - const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' }) + const { doc } = await client.findByID({ id: id!, auth: true, slug: 'date-fields' }) + + await expect(() => { + expect(doc.default).toEqual('2023-02-07T12:00:00.000Z') + }).toPass({ timeout: 10000, intervals: [100] }) + }) + }) + }) + + describe('dates with timezones', () => { + /** + * For now we can only configure one timezone for this entire test suite because the .use is not isolating it per test block + * The last .use options always overrides the rest + * + * See: https://github.com/microsoft/playwright/issues/27138 + */ + test.use({ + timezoneId: londonTimezone, + }) + + test('should display the value in the selected time', async () => { + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + const expectedValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time + const expectedUTCValue = '2027-08-12T01:00:00.000Z' // This is the expected UTC value for the above + const expectedTimezone = 'Asia/Tokyo' + + await expect(dateTimeLocator).toHaveValue(expectedValue) + + await expect(() => { + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) + expect(existingDoc?.dayAndTimeWithTimezone_tz).toEqual(expectedTimezone) + }).toPass({ timeout: 10000, intervals: [100] }) + }) + + test('changing the timezone should update the date to the new equivalent', async () => { + // Tests to see if the date value is updated when the timezone is changed, + // it should change to the equivalent time in the new timezone as the UTC value remains the same + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + const initialDateValue = await dateTimeLocator.inputValue() + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("London")` + + await page.click(dropdownControlSelector) + + await page.click(timezoneOptionSelector) + + // todo: fix + await expect(dateTimeLocator).not.toHaveValue(initialDateValue) + }) + + test('can change timezone inside a block', async () => { + // Tests to see if the date value is updated when the timezone is changed, + // it should change to the equivalent time in the new timezone as the UTC value remains the same + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const dateTimeLocator = page.locator( + '#field-timezoneBlocks__0__dayAndTime .react-datepicker-wrapper input', + ) + + const initialDateValue = await dateTimeLocator.inputValue() + + const dropdownControlSelector = `#field-timezoneBlocks__0__dayAndTime .rs__control` + const timezoneOptionSelector = `#field-timezoneBlocks__0__dayAndTime .rs__menu .rs__option:has-text("London")` + + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + + await expect(dateTimeLocator).not.toHaveValue(initialDateValue) + }) + + test('can change timezone inside an array', async () => { + // Tests to see if the date value is updated when the timezone is changed, + // it should change to the equivalent time in the new timezone as the UTC value remains the same + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const dateTimeLocator = page.locator( + '#field-timezoneArray__0__dayAndTime .react-datepicker-wrapper input', + ) + + const initialDateValue = await dateTimeLocator.inputValue() + + const dropdownControlSelector = `#field-timezoneArray__0__dayAndTime .rs__control` + + const timezoneOptionSelector = `#field-timezoneArray__0__dayAndTime .rs__menu .rs__option:has-text("London")` + + await page.click(dropdownControlSelector) + + await page.click(timezoneOptionSelector) + + await expect(dateTimeLocator).not.toHaveValue(initialDateValue) + }) + + test('can see custom timezone in timezone picker', async () => { + // Tests to see if the date value is updated when the timezone is changed, + // it should change to the equivalent time in the new timezone as the UTC value remains the same + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Monterrey")` + + await page.click(dropdownControlSelector) + + const timezoneOption = page.locator(timezoneOptionSelector) + + await expect(timezoneOption).toBeVisible() + }) + + test('can see default timezone in timezone picker', async () => { + await page.goto(url.create) + + const selectedTimezoneSelector = `#field-dayAndTimeWithTimezone .rs__value-container` + + const selectedTimezone = page.locator(selectedTimezoneSelector) + + await expect(selectedTimezone).toContainText('Monterrey') + }) + + test('can see an error when the date field is required and timezone is empty', async () => { + await page.goto(url.create) + + const dateField = page.locator('#field-default input') + await expect(dateField).toBeVisible() + await dateField.fill('02/07/2023') + await expect(dateField).toHaveValue('02/07/2023') + + // Fill in the date but don't select a timezone + const dateWithTz = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + await dateWithTz.fill('08/12/2027 10:00 AM') + + const timezoneClearButton = page.locator( + `#field-dayAndTimeWithTimezone .rs__control .clear-indicator`, + ) + await timezoneClearButton.click() + + // Expect an error here + await saveDocAndAssert(page, undefined, 'error') + + const errorMessage = page.locator( + '#field-dayAndTimeWithTimezone .field-error .tooltip-content:has-text("A timezone is required.")', + ) + + await expect(errorMessage).toBeVisible() + }) + + test('can clear a selected timezone', async () => { + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const dateField = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + const initialDate = await dateField.inputValue() + + const timezoneClearButton = page.locator( + `#field-dayAndTimeWithTimezone .rs__control .clear-indicator`, + ) + await timezoneClearButton.click() + + const updatedDate = dateField.inputValue() + + await expect(() => { + expect(updatedDate).not.toEqual(initialDate) + }).toPass({ timeout: 10000, intervals: [100] }) + }) + + test('creates the expected UTC value when the timezone is Tokyo', async () => { + // We send this value through the input + const expectedDateInput = 'Jan 1, 2025 6:00 PM' + + const expectedUTCValue = '2025-01-01T09:00:00.000Z' + + await page.goto(url.create) + + const dateField = page.locator('#field-default input') + await dateField.fill('01/01/2025') + + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Tokyo")` + + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await dateTimeLocator.fill(expectedDateInput) + + await saveDocAndAssert(page) + + const docID = page.url().split('/').pop() + + // eslint-disable-next-line payload/no-flaky-assertions + expect(docID).toBeTruthy() + + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + where: { + id: { + equals: docID, + }, + }, + }) + + // eslint-disable-next-line payload/no-flaky-assertions + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) + }) + + test('creates the expected UTC value when the timezone is Paris - no daylight savings', async () => { + // We send this value through the input + const expectedDateInput = 'Jan 1, 2025 6:00 PM' + // We're testing this specific date because Paris has no daylight savings time in January + // but the UTC date will be different from 6PM local time in the summer versus the winter + const expectedUTCValue = '2025-01-01T17:00:00.000Z' + + await page.goto(url.create) + + const dateField = page.locator('#field-default input') + await dateField.fill('01/01/2025') + + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` + + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await dateTimeLocator.fill(expectedDateInput) + + await saveDocAndAssert(page) + + const docID = page.url().split('/').pop() + + // eslint-disable-next-line payload/no-flaky-assertions + expect(docID).toBeTruthy() + + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + where: { + id: { + equals: docID, + }, + }, + }) + + // eslint-disable-next-line payload/no-flaky-assertions + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) + }) + + test('creates the expected UTC value when the timezone is Paris - with daylight savings', async () => { + // We send this value through the input + const expectedDateInput = 'Jul 1, 2025 6:00 PM' + + // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) + // but the UTC date will be different from 6PM local time in the summer versus the winter + const expectedUTCValue = '2025-07-01T16:00:00.000Z' + + await page.goto(url.create) + + const dateField = page.locator('#field-default input') + await dateField.fill('01/01/2025') + + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` + + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await dateTimeLocator.fill(expectedDateInput) + + await saveDocAndAssert(page) + + const docID = page.url().split('/').pop() + + // eslint-disable-next-line payload/no-flaky-assertions + expect(docID).toBeTruthy() + + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + where: { + id: { + equals: docID, + }, + }, + }) + + // eslint-disable-next-line payload/no-flaky-assertions + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) + }) + + describe('while timezone is set to London', () => { + test('displayed value should be the same while timezone is set to London', async () => { + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const result = await page.evaluate(() => { + return Intl.DateTimeFormat().resolvedOptions().timeZone + }) + + await expect(() => { + // Confirm that the emulated timezone is set to London + expect(result).toEqual(londonTimezone) + }).toPass({ timeout: 10000, intervals: [100] }) + + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + const expectedValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time - expect(doc.default).toEqual('2023-02-07T12:00:00.000Z') + await expect(dateTimeLocator).toHaveValue(expectedValue) }) }) }) diff --git a/test/fields/collections/Date/index.ts b/test/fields/collections/Date/index.ts index f157e6b53f8..b6755fb3b6b 100644 --- a/test/fields/collections/Date/index.ts +++ b/test/fields/collections/Date/index.ts @@ -61,6 +61,60 @@ const DateFields: CollectionConfig = { }, }, }, + { + name: 'defaultWithTimezone', + type: 'date', + timezone: true, + }, + { + name: 'dayAndTimeWithTimezone', + type: 'date', + required: true, + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + description: 'This date here should be required.', + }, + timezone: true, + }, + { + type: 'blocks', + name: 'timezoneBlocks', + blocks: [ + { + slug: 'dateBlock', + fields: [ + { + name: 'dayAndTime', + type: 'date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + }, + timezone: true, + }, + ], + }, + ], + }, + { + type: 'array', + name: 'timezoneArray', + fields: [ + { + name: 'dayAndTime', + type: 'date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + }, + timezone: true, + }, + ], + }, ], } diff --git a/test/fields/collections/Date/shared.ts b/test/fields/collections/Date/shared.ts index b3b8b476779..9e196c253c1 100644 --- a/test/fields/collections/Date/shared.ts +++ b/test/fields/collections/Date/shared.ts @@ -7,4 +7,21 @@ export const dateDoc: Partial = { dayOnly: '2022-08-11T22:00:00.000+00:00', dayAndTime: '2022-08-12T10:00:00.052+00:00', monthOnly: '2022-07-31T22:00:00.000+00:00', + defaultWithTimezone: '2027-08-12T10:00:00.000+00:00', + defaultWithTimezone_tz: 'Europe/London', + dayAndTimeWithTimezone: '2027-08-12T01:00:00.000+00:00', // 10AM tokyo time — we will test for this in e2e + dayAndTimeWithTimezone_tz: 'Asia/Tokyo', + timezoneBlocks: [ + { + blockType: 'dateBlock', + dayAndTime: '2025-01-31T09:00:00.000Z', + dayAndTime_tz: 'Europe/Berlin', + }, + ], + timezoneArray: [ + { + dayAndTime: '2025-01-31T09:00:00.549Z', + dayAndTime_tz: 'Europe/Berlin', + }, + ], } diff --git a/test/fields/config.ts b/test/fields/config.ts index 2c0f4ecc89f..3e507e3eeea 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -2,6 +2,7 @@ import type { CollectionConfig } from 'payload' import { fileURLToPath } from 'node:url' import path from 'path' + const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -129,6 +130,13 @@ export default buildConfigWithDefaults({ 'new-value': 'client available', }, }, + timezone: { + supportedTimezones: ({ defaultTimezones }) => [ + ...defaultTimezones, + { label: '(GMT-6) Monterrey, Nuevo Leon', value: 'America/Monterrey' }, + ], + defaultTimezone: 'America/Monterrey', + }, }, localization: { defaultLocale: 'en', diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index a9c08c8c4cf..279be251c15 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -6,6 +6,60 @@ * and re-run `payload generate:types` to regenerate this file. */ +/** + * Supported timezones in IANA format. + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "supportedTimezones". + */ +export type SupportedTimezones = + | 'Pacific/Midway' + | 'Pacific/Niue' + | 'Pacific/Honolulu' + | 'Pacific/Rarotonga' + | 'America/Anchorage' + | 'Pacific/Gambier' + | 'America/Los_Angeles' + | 'America/Tijuana' + | 'America/Denver' + | 'America/Phoenix' + | 'America/Chicago' + | 'America/Guatemala' + | 'America/New_York' + | 'America/Bogota' + | 'America/Caracas' + | 'America/Santiago' + | 'America/Buenos_Aires' + | 'America/Sao_Paulo' + | 'Atlantic/South_Georgia' + | 'Atlantic/Azores' + | 'Atlantic/Cape_Verde' + | 'Europe/London' + | 'Europe/Berlin' + | 'Africa/Lagos' + | 'Europe/Athens' + | 'Africa/Cairo' + | 'Europe/Moscow' + | 'Asia/Riyadh' + | 'Asia/Dubai' + | 'Asia/Baku' + | 'Asia/Karachi' + | 'Asia/Tashkent' + | 'Asia/Calcutta' + | 'Asia/Dhaka' + | 'Asia/Almaty' + | 'Asia/Jakarta' + | 'Asia/Bangkok' + | 'Asia/Shanghai' + | 'Asia/Singapore' + | 'Asia/Tokyo' + | 'Asia/Seoul' + | 'Australia/Sydney' + | 'Pacific/Guam' + | 'Pacific/Noumea' + | 'Pacific/Auckland' + | 'Pacific/Fiji' + | 'America/Monterrey'; /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "BlockColumns". @@ -1067,6 +1121,29 @@ export interface DateField { dayOnly?: string | null; dayAndTime?: string | null; monthOnly?: string | null; + defaultWithTimezone?: string | null; + defaultWithTimezone_tz?: SupportedTimezones; + /** + * This date here should be required. + */ + dayAndTimeWithTimezone: string; + dayAndTimeWithTimezone_tz: SupportedTimezones; + timezoneBlocks?: + | { + dayAndTime?: string | null; + dayAndTime_tz?: SupportedTimezones; + id?: string | null; + blockName?: string | null; + blockType: 'dateBlock'; + }[] + | null; + timezoneArray?: + | { + dayAndTime?: string | null; + dayAndTime_tz?: SupportedTimezones; + id?: string | null; + }[] + | null; updatedAt: string; createdAt: string; } @@ -2714,6 +2791,29 @@ export interface DateFieldsSelect { dayOnly?: T; dayAndTime?: T; monthOnly?: T; + defaultWithTimezone?: T; + defaultWithTimezone_tz?: T; + dayAndTimeWithTimezone?: T; + dayAndTimeWithTimezone_tz?: T; + timezoneBlocks?: + | T + | { + dateBlock?: + | T + | { + dayAndTime?: T; + dayAndTime_tz?: T; + id?: T; + blockName?: T; + }; + }; + timezoneArray?: + | T + | { + dayAndTime?: T; + dayAndTime_tz?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; } diff --git a/test/package.json b/test/package.json index 72b73a0284b..4817eb326fd 100644 --- a/test/package.json +++ b/test/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@aws-sdk/client-s3": "^3.614.0", + "@date-fns/tz": "1.2.0", "@next/env": "15.1.5", "@payloadcms/db-mongodb": "workspace:*", "@payloadcms/db-postgres": "workspace:*",