From 56bfcc08d07ae9249a9c418b1b7ee81abb9739d1 Mon Sep 17 00:00:00 2001 From: PeopleSea <70972819+People-Sea@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:06:27 +0800 Subject: [PATCH 1/3] feat: add StringStateCast for nullable string state handling --- .../Components/StateCasts/StringStateCast.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/schemas/src/Components/StateCasts/StringStateCast.php diff --git a/packages/schemas/src/Components/StateCasts/StringStateCast.php b/packages/schemas/src/Components/StateCasts/StringStateCast.php new file mode 100644 index 0000000000..a743d4884a --- /dev/null +++ b/packages/schemas/src/Components/StateCasts/StringStateCast.php @@ -0,0 +1,30 @@ +isNullable && blank($state)) { + return null; + } + + return (string) $state; + } + + public function set(mixed $state): ?string + { + if ($this->isNullable && blank($state)) { + return null; + } + + return (string) $state; + } +} From f58c3463686f2d3b4524d4cd4a6dece1adf10cc8 Mon Sep 17 00:00:00 2001 From: PeopleSea <70972819+People-Sea@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:07:44 +0800 Subject: [PATCH 2/3] feat: add StringStateCast to DateTimePicker default state casts --- packages/forms/src/Components/DateTimePicker.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/forms/src/Components/DateTimePicker.php b/packages/forms/src/Components/DateTimePicker.php index f96deb73d1..7f93153d42 100644 --- a/packages/forms/src/Components/DateTimePicker.php +++ b/packages/forms/src/Components/DateTimePicker.php @@ -8,6 +8,8 @@ use Carbon\Exceptions\InvalidFormatException; use Closure; use DateTime; use Filament\Schemas\Components\Contracts\HasAffixActions; +use Filament\Schemas\Components\StateCasts\Contracts\StateCast; +use Filament\Schemas\Components\StateCasts\StringStateCast; use Filament\Support\Concerns\HasExtraAlpineAttributes; use Filament\Support\Facades\FilamentTimezone; use Filament\Support\Icons\Heroicon; @@ -149,6 +151,17 @@ class DateTimePicker extends Field implements HasAffixActions ); } + /** + * @return array + */ + public function getDefaultStateCasts(): array + { + return [ + ...parent::getDefaultStateCasts(), + app(StringStateCast::class, ['isNullable' => true]), + ]; + } + public function displayFormat(string | Closure | null $format): static { $this->displayFormat = $format; From 8b911857483ebbea7729dc58111f704abe9a24f0 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Sun, 14 Sep 2025 12:10:19 +0100 Subject: [PATCH 3/3] Implement full state cast to ensure timezones are handled correctly --- .../forms/src/Components/DateTimePicker.php | 84 +++++-------------- packages/forms/src/Components/Select.php | 15 ---- .../src/Components/Concerns/HasState.php | 4 +- .../StateCasts/DateTimeStateCast.php | 56 +++++++++++++ .../Components/StateCasts/StringStateCast.php | 30 ------- 5 files changed, 81 insertions(+), 108 deletions(-) create mode 100644 packages/schemas/src/Components/StateCasts/DateTimeStateCast.php delete mode 100644 packages/schemas/src/Components/StateCasts/StringStateCast.php diff --git a/packages/forms/src/Components/DateTimePicker.php b/packages/forms/src/Components/DateTimePicker.php index 7f93153d42..bd6a50b423 100644 --- a/packages/forms/src/Components/DateTimePicker.php +++ b/packages/forms/src/Components/DateTimePicker.php @@ -9,7 +9,7 @@ use Closure; use DateTime; use Filament\Schemas\Components\Contracts\HasAffixActions; use Filament\Schemas\Components\StateCasts\Contracts\StateCast; -use Filament\Schemas\Components\StateCasts\StringStateCast; +use Filament\Schemas\Components\StateCasts\DateTimeStateCast; use Filament\Support\Concerns\HasExtraAlpineAttributes; use Filament\Support\Facades\FilamentTimezone; use Filament\Support\Icons\Heroicon; @@ -86,65 +86,6 @@ class DateTimePicker extends Field implements HasAffixActions { parent::setUp(); - $this->afterStateHydrated(static function (DateTimePicker $component, $state): void { - if (blank($state)) { - return; - } - - if (! $state instanceof CarbonInterface) { - try { - $state = Carbon::createFromFormat($component->getFormat(), (string) $state, config('app.timezone')); - } catch (InvalidFormatException $exception) { - try { - $state = Carbon::parse($state, config('app.timezone')); - } catch (InvalidFormatException $exception) { - $component->state(null); - - return; - } - } - } - - $state = $state->setTimezone($component->getTimezone()); - - if (! $component->isNative()) { - $component->state((string) $state); - - return; - } - - if (! $component->hasTime()) { - $component->state($state->toDateString()); - - return; - } - - $precision = $component->hasSeconds() ? 'second' : 'minute'; - - if (! $component->hasDate()) { - $component->state($state->toTimeString($precision)); - - return; - } - - $component->state($state->toDateTimeString($precision)); - }); - - $this->dehydrateStateUsing(static function (DateTimePicker $component, $state) { - if (blank($state)) { - return null; - } - - if (! $state instanceof CarbonInterface) { - $state = Carbon::parse($state); - } - - $state->shiftTimezone($component->getTimezone()); - $state->setTimezone(config('app.timezone')); - - return $state->format($component->getFormat()); - }); - $this->rule( 'date', static fn (DateTimePicker $component): bool => $component->hasDate(), @@ -158,10 +99,31 @@ class DateTimePicker extends Field implements HasAffixActions { return [ ...parent::getDefaultStateCasts(), - app(StringStateCast::class, ['isNullable' => true]), + app(DateTimeStateCast::class, [ + 'format' => $this->getFormat(), + 'internalFormat' => $this->getInternalFormat(), + 'timezone' => $this->getTimezone(), + ]), ]; } + public function getInternalFormat(): string + { + if (! $this->isNative()) { + return 'Y-m-d H:i:s'; + } + + if (! $this->hasTime()) { + return 'Y-m-d'; + } + + if (! $this->hasDate()) { + return $this->hasSeconds() ? 'H:i:s' : 'H:i'; + } + + return 'Y-m-d H:i:s'; + } + public function displayFormat(string | Closure | null $format): static { $this->displayFormat = $format; diff --git a/packages/forms/src/Components/Select.php b/packages/forms/src/Components/Select.php index d7c5424c35..4554b5fdbf 100644 --- a/packages/forms/src/Components/Select.php +++ b/packages/forms/src/Components/Select.php @@ -1367,21 +1367,6 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has return (bool) $this->evaluate($this->canOptionLabelsWrap); } - public function hydrateDefaultState(?array &$hydratedDefaultState): void - { - parent::hydrateDefaultState($hydratedDefaultState); - - if (is_bool($state = $this->getState())) { - $state = $state ? 1 : 0; - - $this->state($state); - - if (is_array($hydratedDefaultState)) { - Arr::set($hydratedDefaultState, $this->getStatePath(), $state); /** @phpstan-ignore parameterByRef.type */ - } - } - } - public function getQualifiedRelatedKeyNameForRelationship(Relation $relationship): string { if ($relationship instanceof BelongsToMany) { diff --git a/packages/schemas/src/Components/Concerns/HasState.php b/packages/schemas/src/Components/Concerns/HasState.php index 278c8ee0ce..4a114e9ab3 100644 --- a/packages/schemas/src/Components/Concerns/HasState.php +++ b/packages/schemas/src/Components/Concerns/HasState.php @@ -444,14 +444,14 @@ trait HasState } if (! $this->hasDefaultState()) { - $this->hasStatePath() && $this->state(null); + $this->hasStatePath() && $this->rawState(null); return; } $defaultState = $this->getDefaultState(); - $this->state($defaultState); + $this->rawState($defaultState); Arr::set($hydratedDefaultState, $statePath, $defaultState); /** @phpstan-ignore parameterByRef.type */ } diff --git a/packages/schemas/src/Components/StateCasts/DateTimeStateCast.php b/packages/schemas/src/Components/StateCasts/DateTimeStateCast.php new file mode 100644 index 0000000000..98c0254ffa --- /dev/null +++ b/packages/schemas/src/Components/StateCasts/DateTimeStateCast.php @@ -0,0 +1,56 @@ +shiftTimezone($this->timezone); + $state->setTimezone(config('app.timezone')); + + return $state->format($this->format); + } + + public function set(mixed $state): ?string + { + if (blank($state)) { + return null; + } + + if (! $state instanceof CarbonInterface) { + try { + $state = Carbon::createFromFormat($this->format, (string) $state, config('app.timezone')); + } catch (InvalidFormatException) { + try { + $state = Carbon::parse($state, config('app.timezone')); + } catch (InvalidFormatException) { + return null; + } + } + } + + $state = $state->setTimezone($this->timezone); + + return $state->format($this->internalFormat); + } +} diff --git a/packages/schemas/src/Components/StateCasts/StringStateCast.php b/packages/schemas/src/Components/StateCasts/StringStateCast.php deleted file mode 100644 index a743d4884a..0000000000 --- a/packages/schemas/src/Components/StateCasts/StringStateCast.php +++ /dev/null @@ -1,30 +0,0 @@ -isNullable && blank($state)) { - return null; - } - - return (string) $state; - } - - public function set(mixed $state): ?string - { - if ($this->isNullable && blank($state)) { - return null; - } - - return (string) $state; - } -}