diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 063a744261a..6d9e22ffd96 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -17,6 +17,7 @@ - Element slideouts now automatically refresh when the same element is updated in another tab/slideout. ([#18625](https://github.com/craftcms/cms/pull/18625)) - Added the “Time Zone” user preference. ([#8518](https://github.com/craftcms/cms/discussions/8518)) - Element indexes now automatically refresh after duplicating elements and the queue is completed, if there’s an active search term. ([#18636](https://github.com/craftcms/cms/issues/18636)) +- Timestamps in the control panel now include their time zone abbreviation. ([#18639](https://github.com/craftcms/cms/pull/18639)) ### Administration - Time fields’ “Max Time” settings can now be set to an earlier time than “Min Time”, for overnight time ranges. ([#18575](https://github.com/craftcms/cms/pull/18575)) @@ -26,6 +27,7 @@ ### Development - Added the `heading()`/`h()` and `h1()`…`h6()` Twig functions. ([#18524](https://github.com/craftcms/cms/pull/18524)) - The `tag()` function now accepts a string for its second argument. ([#18524](https://github.com/craftcms/cms/pull/18524)) +- The `|time` and `|datetime` Twig filters now have `$withTimeZone` arguments. ([#18639](https://github.com/craftcms/cms/pull/18639)) - `delete` GraphQL queries now have a `hardDelete` argument. ([#18511](https://github.com/craftcms/cms/pull/18511)) - Assets’ `url` GraphQL fields’ `immediately` arguments are no longer deprecated. ([#18581](https://github.com/craftcms/cms/issues/18581)) - `craft\fields\data\LinkData::getUrl()` now has an `$anyStatus` argument, which can be set to `false` to prevent a value from being returned if a disabled/pending/expired element is linked. ([#18527](https://github.com/craftcms/cms/issues/18527)) @@ -38,6 +40,7 @@ - Added `Craft.CpScreenSlideout::reload()`. ([#18625](https://github.com/craftcms/cms/pull/18625)) - `craft\elements\PopulateElementEvent::$row` no longer includes `fieldValues` or `generatedFieldValues` keys. - `craft\helpers\DateTimeHelper::timeZoneAbbreviation()` is no longer deprecated, and now has a `$date` argument. +- `craft\i18n\Formatter::asTime()` and `asDatetime()` now have `$withTimeZone` arguments. ([#18639](https://github.com/craftcms/cms/pull/18639)) - `Craft.CP` now triggers a `queueCompleted` event when the last queue job is completed. ### System diff --git a/src/base/Element.php b/src/base/Element.php index 0cdbb598ba8..70df350782a 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -6456,10 +6456,10 @@ public function getMetadata(): array }, ], $metadata, [ Craft::t('app', 'Created at') => $this->dateCreated && !$this->getIsUnpublishedDraft() - ? $formatter->asDatetime($this->dateCreated, Formatter::FORMAT_WIDTH_SHORT) + ? $formatter->asDatetime($this->dateCreated, Formatter::FORMAT_WIDTH_SHORT, true) : false, Craft::t('app', 'Updated at') => $this->dateUpdated && !$this->getIsUnpublishedDraft() - ? $formatter->asDatetime($this->dateUpdated, Formatter::FORMAT_WIDTH_SHORT) + ? $formatter->asDatetime($this->dateUpdated, Formatter::FORMAT_WIDTH_SHORT, true) : false, Craft::t('app', 'Notes') => function() { if ($this->getIsRevision()) { diff --git a/src/controllers/AssetsController.php b/src/controllers/AssetsController.php index d7094858e71..a48e09b21c7 100644 --- a/src/controllers/AssetsController.php +++ b/src/controllers/AssetsController.php @@ -483,7 +483,11 @@ public function actionReplaceFile(): Response 'filename' => $resultingAsset->getFilename(), 'formattedSize' => $resultingAsset->getFormattedSize(0), 'formattedSizeInBytes' => $resultingAsset->getFormattedSizeInBytes(false), - 'formattedDateUpdated' => Craft::$app->getFormatter()->asDatetime($resultingAsset->dateUpdated, Formatter::FORMAT_WIDTH_SHORT), + 'formattedDateUpdated' => Craft::$app->getFormatter()->asDatetime( + $resultingAsset->dateUpdated, + Formatter::FORMAT_WIDTH_SHORT, + true, + ), 'dimensions' => $resultingAsset->getDimensions(), 'updatedTimestamp' => $resultingAsset->dateUpdated->getTimestamp(), 'resultingUrl' => $resultingAsset->getUrl(), diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 22575a5414e..c334b824ed8 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -890,7 +890,7 @@ private function _contextMenuItems( /** @var ElementInterface&DraftBehavior $draft */ $creator = $draft->getCreator(); $timestamp = $formatter->asTimestamp($draft->dateUpdated, Locale::LENGTH_SHORT, true); - $timestampWithDate = $formatter->asDatetime($draft->dateUpdated, Locale::LENGTH_SHORT); + $timestampWithDate = $formatter->asDatetime($draft->dateUpdated, Locale::LENGTH_SHORT, true); return [ 'label' => $draft->draftName, @@ -921,7 +921,7 @@ private function _contextMenuItems( /** @var ElementInterface&RevisionBehavior $revision */ $creator = $revision->getCreator(); $timestamp = $formatter->asTimestamp($revision->dateCreated, Locale::LENGTH_SHORT, true); - $timestampWithDate = $formatter->asDatetime($revision->dateCreated, Locale::LENGTH_SHORT); + $timestampWithDate = $formatter->asDatetime($revision->dateCreated, Locale::LENGTH_SHORT, true); return [ 'label' => $revision->getRevisionLabel(), diff --git a/src/elements/Entry.php b/src/elements/Entry.php index 4a0d3058144..e913f76ae1e 100644 --- a/src/elements/Entry.php +++ b/src/elements/Entry.php @@ -2961,7 +2961,7 @@ public function beforeSave(bool $isNew): bool Craft::$app->getRevisions()->createRevision( $current, $current->getAuthorId(), - sprintf('Revision from %s', Craft::$app->getFormatter()->asDatetime($current->dateUpdated)), + sprintf('Revision from %s', Craft::$app->getFormatter()->asDatetime($current->dateUpdated, withTimeZone: true)), ); } } diff --git a/src/elements/User.php b/src/elements/User.php index 026135448a7..58692ec3860 100644 --- a/src/elements/User.php +++ b/src/elements/User.php @@ -2522,7 +2522,7 @@ protected function metadata(): array } return $formatter->asDuration($duration); }, - Craft::t('app', 'Created at') => $formatter->asDatetime($this->dateCreated, Formatter::FORMAT_WIDTH_SHORT), + Craft::t('app', 'Created at') => $formatter->asDatetime($this->dateCreated, Formatter::FORMAT_WIDTH_SHORT, true), Craft::t('app', 'Last login') => function() use ($formatter) { if ($this->pending) { return false; @@ -2530,13 +2530,13 @@ protected function metadata(): array if (!$this->lastLoginDate) { return Craft::t('app', 'Never'); } - return $formatter->asDatetime($this->lastLoginDate, Formatter::FORMAT_WIDTH_SHORT); + return $formatter->asDatetime($this->lastLoginDate, Formatter::FORMAT_WIDTH_SHORT, true); }, Craft::t('app', 'Last login fail') => function() use ($formatter) { if (!$this->locked || !$this->lastInvalidLoginDate) { return false; } - return $formatter->asDatetime($this->lastInvalidLoginDate, Formatter::FORMAT_WIDTH_SHORT); + return $formatter->asDatetime($this->lastInvalidLoginDate, Formatter::FORMAT_WIDTH_SHORT, true); }, Craft::t('app', 'Login fail count') => function() use ($formatter) { if (!$this->locked) { diff --git a/src/fields/Date.php b/src/fields/Date.php index 31a6a7ddb9b..cf807e7dd53 100644 --- a/src/fields/Date.php +++ b/src/fields/Date.php @@ -356,16 +356,12 @@ public function getPreviewHtml(mixed $value, ElementInterface $element): string if ($this->showTimeZone) { $timeZone = $formatter->timeZone; $formatter->timeZone = $value->getTimezone()->getName(); - $html = sprintf( - '%s %s', - $formatter->asDatetime($value, Locale::LENGTH_SHORT), - DateTimeHelper::timeZoneAbbreviation($value->getTimezone(), $value), - ); + $html = $formatter->asDatetime($value, Locale::LENGTH_SHORT, true); $formatter->timeZone = $timeZone; return $html; } - return $formatter->asDatetime($value, Locale::LENGTH_SHORT); + return $formatter->asDatetime($value, Locale::LENGTH_SHORT, true); } if ($this->showDate) { diff --git a/src/gql/directives/FormatDateTime.php b/src/gql/directives/FormatDateTime.php index 817bec2bb35..7d6c13ab3da 100644 --- a/src/gql/directives/FormatDateTime.php +++ b/src/gql/directives/FormatDateTime.php @@ -60,6 +60,12 @@ public static function create(): GqlDirective 'type' => Type::string(), 'description' => 'The locale to use when formatting the date. (E.g., en-US)', ]), + new FieldArgument([ + 'name' => 'withTimeZone', + 'type' => Type::boolean(), + 'description' => 'Whether the time zone abbreviation should be appended to the formatted time.', + 'defaultValue' => false, + ]), ], 'description' => 'Formats a date in the desired format. Can be applied to all fields, only changes output of DateTime fields.', ])); @@ -108,7 +114,7 @@ public static function apply(mixed $source, mixed $value, array $arguments, Reso $formatter->timeZone = $timezone; - $value = $formatter->asDatetime($value, $format); + $value = $formatter->asDatetime($value, $format, $arguments['withTimeZone'] ?? false); } return $value; diff --git a/src/helpers/ElementHelper.php b/src/helpers/ElementHelper.php index a6dfa17d88b..7089862d387 100644 --- a/src/helpers/ElementHelper.php +++ b/src/helpers/ElementHelper.php @@ -818,7 +818,7 @@ public static function attributeHtml(mixed $value): string if ($value instanceof DateTime) { $formatter = Craft::$app->getFormatter(); return Html::tag('span', $formatter->asTimestamp($value, Locale::LENGTH_SHORT), [ - 'title' => $formatter->asDatetime($value, Locale::LENGTH_SHORT), + 'title' => $formatter->asDatetime($value, Locale::LENGTH_SHORT, true), ]); } diff --git a/src/i18n/Formatter.php b/src/i18n/Formatter.php index 5964952c326..0434426ff18 100644 --- a/src/i18n/Formatter.php +++ b/src/i18n/Formatter.php @@ -146,11 +146,12 @@ private function formatDateWithoutIntl(int|string|DateTime $value, ?string $form * @inheritdoc * @param int|string|DateTime $value * @param string|null $format + * @param bool $withTimeZone Whether the time zone abbreviation should be appended to the formatted time * @return string * @throws InvalidArgumentException * @throws InvalidConfigException */ - public function asTime($value, $format = null): string + public function asTime($value, $format = null, bool $withTimeZone = false): string { if ($format === null) { $format = $this->timeFormat; @@ -161,22 +162,31 @@ public function asTime($value, $format = null): string } if (str_starts_with($format, 'php:')) { - return $this->_formatDateTimeValueWithPhpFormat($value, substr($format, 4), 'time'); + $result = $this->_formatDateTimeValueWithPhpFormat($value, substr($format, 4), 'time'); + } else { + $result = parent::asTime($value, $format); + } + + if ($withTimeZone && $result && $result !== $this->nullDisplay) { + $result .= ' ' . DateTimeHelper::timeZoneAbbreviation($value->getTimezone()); } - return parent::asTime($value, $format); + return $result; } /** * @inheritdoc * @param int|string|DateTime $value * @param string|null $format + * @param bool $withTimeZone Whether the time zone abbreviation should be appended to the formatted time * @return string * @throws InvalidArgumentException * @throws InvalidConfigException */ - public function asDatetime($value, $format = null): string + public function asDatetime($value, $format = null, bool $withTimeZone = false): string { + $value = DateTimeHelper::toDateTime($value, false, false); + if ($format === null) { $format = $this->datetimeFormat; } @@ -186,10 +196,16 @@ public function asDatetime($value, $format = null): string } if (str_starts_with($format, 'php:')) { - return $this->_formatDateTimeValueWithPhpFormat($value, substr($format, 4), 'datetime'); + $result = $this->_formatDateTimeValueWithPhpFormat($value, substr($format, 4), 'datetime'); + } else { + $result = parent::asDatetime($value, $format); + } + + if ($withTimeZone && $result && $result !== $this->nullDisplay) { + $result .= ' ' . DateTimeHelper::timeZoneAbbreviation($value->getTimezone()); } - return parent::asDatetime($value, $format); + return $result; } /** diff --git a/src/queue/Queue.php b/src/queue/Queue.php index 5938530699e..ed4283a1ab8 100644 --- a/src/queue/Queue.php +++ b/src/queue/Queue.php @@ -503,9 +503,9 @@ public function getJobDetails(string $id): array 'job' => $job, 'ttr' => (int)$result['ttr'], 'Priority' => $result['priority'], - 'Pushed at' => $result['timePushed'] ? $formatter->asDatetime($result['timePushed']) : '', - 'Updated at' => $result['timeUpdated'] ? $formatter->asDatetime($result['timeUpdated']) : '', - 'Failed at' => $result['dateFailed'] ? $formatter->asDatetime($result['dateFailed']) : '', + 'Pushed at' => $result['timePushed'] ? $formatter->asDatetime($result['timePushed'], withTimeZone: true) : '', + 'Updated at' => $result['timeUpdated'] ? $formatter->asDatetime($result['timeUpdated'], withTimeZone: true) : '', + 'Failed at' => $result['dateFailed'] ? $formatter->asDatetime($result['dateFailed'], withTimeZone: true) : '', ]); } diff --git a/src/templates/_components/utilities/Migrations.twig b/src/templates/_components/utilities/Migrations.twig index e6bb43172db..fbf0c21e005 100644 --- a/src/templates/_components/utilities/Migrations.twig +++ b/src/templates/_components/utilities/Migrations.twig @@ -35,7 +35,7 @@ {{ migrationName }} {{ 'Applied'|t('app') }} - {{ migration|datetime() }} + {{ migration|datetime(withTimeZone: true) }} {% endfor %} diff --git a/src/templates/_elements/revisions.twig b/src/templates/_elements/revisions.twig index 51ad8c1132f..6e2e95bbd10 100644 --- a/src/templates/_elements/revisions.twig +++ b/src/templates/_elements/revisions.twig @@ -16,7 +16,7 @@ {{ revisionLabel }} - {{ revision.dateCreated|datetime('short') }} + {{ revision.dateCreated|datetime('short', withTimeZone: true) }} {% if creator %} {{ elementChip(creator) }} diff --git a/src/views/debug/deprecated/traces.php b/src/views/debug/deprecated/traces.php index a6f312802c2..ac6c21af638 100644 --- a/src/views/debug/deprecated/traces.php +++ b/src/views/debug/deprecated/traces.php @@ -22,7 +22,7 @@ ], [ Craft::t('app', 'Last Occurrence'), - Craft::$app->getFormatter()->asDatetime($log->lastOccurrence, 'short'), + Craft::$app->getFormatter()->asDatetime($log->lastOccurrence, \craft\i18n\Locale::LENGTH_SHORT, true), ], ], ]); diff --git a/src/web/twig/Extension.php b/src/web/twig/Extension.php index 75f5d447475..452822f73f3 100644 --- a/src/web/twig/Extension.php +++ b/src/web/twig/Extension.php @@ -1066,10 +1066,17 @@ public function rssFilter(TwigEnvironment $env, mixed $date, mixed $timezone = n * @param string|null $format The target format, null to use the default * @param DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * @param string|null $locale The target locale the date should be formatted for. By default, the current system locale will be used. + * @param bool $withTimeZone Whether the time zone abbreviation should be appended to the formatted time * @return string */ - public function timeFilter(TwigEnvironment $env, mixed $date, ?string $format = null, mixed $timezone = null, ?string $locale = null): string - { + public function timeFilter( + TwigEnvironment $env, + mixed $date, + ?string $format = null, + mixed $timezone = null, + ?string $locale = null, + bool $withTimeZone = false, + ): string { // Is this a custom PHP date format? if ($format !== null && !in_array($format, [Locale::LENGTH_SHORT, Locale::LENGTH_MEDIUM, Locale::LENGTH_LONG, Locale::LENGTH_FULL], true)) { if (str_starts_with($format, 'icu:')) { @@ -1079,11 +1086,11 @@ public function timeFilter(TwigEnvironment $env, mixed $date, ?string $format = } } - $date = $env->getExtension(CoreExtension::class)->convertDate($date, $timezone); + $date = DateTime::createFromInterface($env->getExtension(CoreExtension::class)->convertDate($date, $timezone)); $formatter = $locale ? Craft::$app->getI18n()->getLocaleById($locale)->getFormatter() : Craft::$app->getFormatter(); $fmtTimeZone = $formatter->timeZone; $formatter->timeZone = $timezone !== null ? $date->getTimezone()->getName() : $formatter->timeZone; - $formatted = $formatter->asTime(DateTime::createFromInterface($date), $format); + $formatted = $formatter->asTime($date, $format, $withTimeZone); $formatter->timeZone = $fmtTimeZone; return $formatted; } @@ -1096,10 +1103,17 @@ public function timeFilter(TwigEnvironment $env, mixed $date, ?string $format = * @param string|null $format The target format, null to use the default * @param DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * @param string|null $locale The target locale the date should be formatted for. By default, the current system locale will be used. + * @param bool $withTimeZone Whether the time zone abbreviation should be appended to the formatted time * @return string */ - public function datetimeFilter(TwigEnvironment $env, mixed $date, ?string $format = null, mixed $timezone = null, ?string $locale = null): string - { + public function datetimeFilter( + TwigEnvironment $env, + mixed $date, + ?string $format = null, + mixed $timezone = null, + ?string $locale = null, + bool $withTimeZone = false, + ): string { // Is this a custom PHP date format? if ($format !== null && !in_array($format, [Locale::LENGTH_SHORT, Locale::LENGTH_MEDIUM, Locale::LENGTH_LONG, Locale::LENGTH_FULL], true)) { if (str_starts_with($format, 'icu:')) { @@ -1113,7 +1127,7 @@ public function datetimeFilter(TwigEnvironment $env, mixed $date, ?string $forma $formatter = $locale ? Craft::$app->getI18n()->getLocaleById($locale)->getFormatter() : Craft::$app->getFormatter(); $fmtTimeZone = $formatter->timeZone; $formatter->timeZone = $timezone !== null ? $date->getTimezone()->getName() : $formatter->timeZone; - $formatted = $formatter->asDatetime(DateTime::createFromInterface($date), $format); + $formatted = $formatter->asDatetime(DateTime::createFromInterface($date), $format, $withTimeZone); $formatter->timeZone = $fmtTimeZone; return $formatted; }