From 75ff9ccef2109d30952237aa62ab3b597227e870 Mon Sep 17 00:00:00 2001 From: Florian Brinkmann Date: Wed, 22 Jan 2025 15:04:09 +0100 Subject: [PATCH] feat: allow different cp timezone from app timezone --- config/system.php | 19 +++++++++++++++ src/Data/AbstractAugmented.php | 9 +++++++ src/Fieldtypes/Date.php | 43 +++++++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/config/system.php b/config/system.php index fc04ee4b02..67841170f3 100644 --- a/config/system.php +++ b/config/system.php @@ -74,6 +74,25 @@ 'date_format' => 'F jS, Y', + /* + |-------------------------------------------------------------------------- + | Client Timezone + |-------------------------------------------------------------------------- + | + | If specified, this timezone is used as the timezone for visible date and + | time values in the control panel and in Antlers. That allows you to keep + | the app timezone in UTC while having, for example, the `Europe/Berlin` + | timezone for the control panel. + | + | That means, if you publish an entry at 2024-05-22 10:15 with the + | client timezone being set to `Europe/Berlin` and the app timezone being + | set to `UTC`, the entry date will be 2024-05-22 08:15 internally, but + | displayed as 2024-05-22 10:15 in the control panel and in antlers. + | + */ + + 'client_timezone' => null, + /* |-------------------------------------------------------------------------- | Default Character Set diff --git a/src/Data/AbstractAugmented.php b/src/Data/AbstractAugmented.php index 2d79ff3eba..3ecce75792 100644 --- a/src/Data/AbstractAugmented.php +++ b/src/Data/AbstractAugmented.php @@ -2,6 +2,7 @@ namespace Statamic\Data; +use Carbon\Carbon; use Statamic\Contracts\Data\Augmented; use Statamic\Fields\Value; use Statamic\Statamic; @@ -55,6 +56,14 @@ private function transientValue($key) $value->setFieldtype($deferred->fieldtype()); $value->setAugmentable($deferred->augmentable()); + if ($deferred->raw() instanceof Carbon) { + $clientTimezone = config('statamic.system.client_timezone'); + $appTimezone = config('app.timezone'); + if ($clientTimezone && $clientTimezone !== $appTimezone) { + $deferred->raw()->setTimezone($clientTimezone); + } + } + return $deferred->raw(); }; diff --git a/src/Fieldtypes/Date.php b/src/Fieldtypes/Date.php index d9aba7a16c..d20609b0fe 100644 --- a/src/Fieldtypes/Date.php +++ b/src/Fieldtypes/Date.php @@ -145,7 +145,7 @@ private function preProcessSingle($value) $value = $value['start']; } - $date = $this->parseSaved($value); + $date = $this->parseSaved($value, true); return $this->splitDateTimeForPreProcessSingle($date); } @@ -168,8 +168,8 @@ private function preProcessRange($value) } return $this->splitDateTimeForPreProcessRange([ - 'start' => $this->parseSaved($value['start'])->format($vueFormat), - 'end' => $this->parseSaved($value['end'])->format($vueFormat), + 'start' => $this->parseSaved($value['start'], true)->format($vueFormat), + 'end' => $this->parseSaved($value['end'], true)->format($vueFormat), ]); } @@ -221,14 +221,28 @@ private function processDateTime($value) { $date = Carbon::parse($value); - return $this->formatAndCast($date, $this->saveFormat()); + return $this->formatAndCast($this->convertTimezoneForSave($date), $this->saveFormat()); } private function processDateTimeEndOfDay($value) { $date = Carbon::parse($value)->endOfDay(); - return $this->formatAndCast($date, $this->saveFormat()); + return $this->formatAndCast($this->convertTimezoneForSave($date), $this->saveFormat()); + } + + private function convertTimezoneForSave(Carbon $date): Carbon + { + $clientTimezone = config('statamic.system.client_timezone'); + $appTimezone = config('app.timezone'); + if (! $clientTimezone || $clientTimezone === $appTimezone) { + return $date; + } + + // Date has the app timezone set, so we need to shift that to the CP timezone first without modifying the time. + return $date + ->shiftTimezone($clientTimezone) + ->setTimezone($appTimezone); } public function preProcessIndex($value) @@ -244,8 +258,8 @@ public function preProcessIndex($value) $value = ['start' => $value, 'end' => $value]; } - $start = $this->parseSaved($value['start'])->format($this->indexDisplayFormat()); - $end = $this->parseSaved($value['end'])->format($this->indexDisplayFormat()); + $start = $this->parseSaved($value['start'], true)->format($this->indexDisplayFormat()); + $end = $this->parseSaved($value['end'], true)->format($this->indexDisplayFormat()); return $start.' - '.$end; } @@ -255,7 +269,7 @@ public function preProcessIndex($value) $value = $value['start']; } - return $this->parseSaved($value)->format($this->indexDisplayFormat()); + return $this->parseSaved($value, true)->format($this->indexDisplayFormat()); } private function saveFormat() @@ -346,13 +360,20 @@ public function toQueryableValue($value) return $this->augment($value); } - private function parseSaved($value) + private function parseSaved($value, $convertTz = false) { try { - return Carbon::createFromFormat($this->saveFormat(), $value); + $carbon = Carbon::createFromFormat($this->saveFormat(), $value); } catch (InvalidFormatException|InvalidArgumentException $e) { - return Carbon::parse($value); + $carbon = Carbon::parse($value); + } + + $clientTimezone = config('statamic.system.client_timezone'); + if ($convertTz && $clientTimezone && $clientTimezone !== config('app.timezone')) { + $carbon->setTimezone($clientTimezone); } + + return $carbon; } public function timeEnabled()