diff --git a/.meta-storm.xml b/.meta-storm.xml index 255f011ba..3e4b1dad8 100644 --- a/.meta-storm.xml +++ b/.meta-storm.xml @@ -28,6 +28,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + lg xl + + prevent + stop + outside + window + document + once + debounce + throttle + self + camel + dot + passive + capture + diff --git a/src/Contracts/src/UI/FieldWithComponentContract.php b/src/Contracts/src/UI/FieldWithComponentContract.php new file mode 100644 index 000000000..a3bc88efc --- /dev/null +++ b/src/Contracts/src/UI/FieldWithComponentContract.php @@ -0,0 +1,16 @@ +toValue())) { + return $this->toValue(); + } + return $this->toValue()?->getKey(); } @@ -122,13 +126,7 @@ public function prepareReactivityValue(mixed $value, mixed &$casted, array &$exc $value = data_get($value, 'value', $value); $casted = $this->getRelatedModel(); - $related = $this->getRelation()?->getRelated(); - - $target = $related?->forceFill([ - $related->getKeyName() => $value, - ]); - - $casted?->setRelation($this->getRelationName(), $target); + $casted?->setRelation($this->getRelationName(), $this->makeRelatedModel($value)); return $value; } diff --git a/src/Laravel/src/Fields/Relationships/BelongsToMany.php b/src/Laravel/src/Fields/Relationships/BelongsToMany.php index 37564f3da..094712fca 100644 --- a/src/Laravel/src/Fields/Relationships/BelongsToMany.php +++ b/src/Laravel/src/Fields/Relationships/BelongsToMany.php @@ -11,8 +11,10 @@ use Illuminate\Support\Collection; use MoonShine\Contracts\Core\DependencyInjection\FieldsContract; use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract; +use MoonShine\Contracts\UI\ActionButtonContract; use MoonShine\Contracts\UI\Collection\ActionButtonsContract; use MoonShine\Contracts\UI\ComponentContract; +use MoonShine\Contracts\UI\FieldWithComponentContract; use MoonShine\Contracts\UI\HasFieldsContract; use MoonShine\Contracts\UI\TableBuilderContract; use MoonShine\Laravel\Collections\Fields; @@ -46,12 +48,14 @@ * * @extends ModelRelationField * @implements HasFieldsContract + * @implements FieldWithComponentContract */ class BelongsToMany extends ModelRelationField implements HasRelatedValuesContact, HasPivotContract, HasFieldsContract, - HasAsyncSearchContract + HasAsyncSearchContract, + FieldWithComponentContract { use WithFields; use WithRelatedValues; @@ -70,8 +74,6 @@ class BelongsToMany extends ModelRelationField implements protected bool $isGroup = true; - protected bool $hasOld = false; - protected bool $resolveValueOnce = true; protected string $treeParentColumn = ''; @@ -100,6 +102,8 @@ class BelongsToMany extends ModelRelationField implements protected ?string $columnLabel = null; + protected ?TableBuilderContract $resolvedComponent = null; + public function onlyCount(): static { $this->onlyCount = true; @@ -187,7 +191,7 @@ public function getRelatedKeyName(): string public function getCollectionValue(): EloquentCollection { - return new EloquentCollection($this->toValue() ?? []); + return new EloquentCollection($this->getValue() ?? []); } public function getSelectedValue(): string|array @@ -222,6 +226,11 @@ protected function getResourceColumnLabel(): string return $this->columnLabel ?? $this->getResource()->getTitle(); } + public function getPivotName(): string + { + return "{$this->getRelationName()}_pivot"; + } + /** * @throws Throwable */ @@ -233,7 +242,7 @@ protected function prepareFields(): FieldsContract ->setColumn("{$this->getPivotAs()}.{$field->getColumn()}") ->class('js-pivot-field') ->withoutWrapper(), - performName: fn (string $name): string => str_replace($this->getRelationName(), "{$this->getRelationName()}_pivot", $name), + performName: fn (string $name): string => str_replace($this->getRelationName(), $this->getPivotName(), $name), ); } @@ -248,19 +257,25 @@ protected function prepareFill(array $raw = [], ?DataWrapperContract $casted = n return $values; } - /** - * @throws Throwable - */ - protected function resolveValue(): mixed + public function setValues(array $values): void { + $this->setValue(new Collection($values)); + } + + public function getAvailableValues(): mixed + { + if (! \is_null($this->memoizeValues)) { + return $this->memoizeValues; + } + // fix for filters if ($this->isAsyncSearch() && ! $this->isValueWithModels($this->memoizeValues) && filled($this->toValue())) { $keys = $this->isSelectMode() ? $this->getCollectionValue()->toArray() : $this->getCollectionValue()->keys(); $this->memoizeValues = $this->getRelation() - ?->getRelated() - ?->newQuery() - ?->findMany($keys) ?? EloquentCollection::make(); + ?->getRelated() + ?->newQuery() + ?->findMany($keys) ?? EloquentCollection::make(); } if ($this->isSelectMode()) { @@ -289,9 +304,13 @@ protected function resolveValue(): mixed }); } - protected function getComponent(): ComponentContract + public function getComponent(): ComponentContract { - $values = $this->getValue(); + if (! \is_null($this->resolvedComponent)) { + return $this->resolvedComponent; + } + + $values = $this->getAvailableValues(); if ($this->isRelatedLink()) { return $this->getRelatedLink(); @@ -326,7 +345,7 @@ protected function getComponent(): ComponentContract ) ->prepend($identityField); - return TableBuilder::make(items: $values) + return $this->resolvedComponent = TableBuilder::make(items: $values) ->name($this->getTableComponentName()) ->customAttributes($this->getAttributes()->jsonSerialize()) ->fields($fields) @@ -359,6 +378,33 @@ protected function getColumnOrFormattedValue(Model $item, string|int $default): return $default; } + protected function resolveOldValue(mixed $old): mixed + { + // otherwise you will have to make a db query to receive records by keys + if ($this->isAsyncSearch()) { + return $this->toValue(); + } + + $oldPivot = $this->getCore()->getRequest()->getOld($this->getPivotName()); + + return collect($old) + ->map(fn ($key): ?Model => clone $this->makeRelatedModel($key, relations: $oldPivot[$key] ?? [])) + ->values(); + } + + protected function resolveValue(): mixed + { + if (\is_array($this->toValue())) { + $this->setValue( + collect($this->toValue()) + ->map(fn ($key): ?Model => clone $this->makeRelatedModel($key)) + ->values() + ); + } + + return parent::resolveValue(); + } + protected function resolveRawValue(): mixed { return $this->getCollectionValue() @@ -443,7 +489,7 @@ public function getCheckedKeys(): Collection public function getKeys(): array { - if (\is_null($this->toValue())) { + if (\is_null($this->getValue())) { return []; } @@ -547,11 +593,9 @@ protected function resolveAfterDestroy(mixed $data): mixed public function prepareReactivityValue(mixed $value, mixed &$casted, array &$except): mixed { $casted = $this->getRelatedModel(); - $related = $this->getRelation()?->getRelated(); - - $value = collect($value)->map(fn ($v) => clone $related->forceFill([ - $related->getKeyName() => $v, - ]))->values(); + $value = collect($value) + ->map(fn ($key): ?Model => clone $this->makeRelatedModel($key)) + ->values(); $casted?->setRelation($this->getRelationName(), $value); $except[$this->getColumn()] = $this->getColumn(); @@ -565,11 +609,7 @@ public function getReactiveValue(): mixed throw FieldException::reactivityNotSupported(static::class, 'with asyncSearch'); } - if (! $this->isTree() || $this->isSelectMode()) { - return $this->getCollectionValue()->pluck($this->getRelatedKeyName()); - } - - return parent::getReactiveValue(); + return $this->getCollectionValue()->pluck($this->getRelatedKeyName()); } /** @@ -598,6 +638,7 @@ protected function viewData(): array return [ ...$viewData, 'isSearchable' => $this->isSearchable(), + 'values' => $this->getAvailableValues(), ]; } @@ -608,12 +649,10 @@ protected function viewData(): array ]; } - $component = $this->getComponent(); - return [ ...$viewData, - 'component' => $component, - 'componentName' => $component->getName(), + 'component' => $this->getComponent(), + 'componentName' => $this->getComponent()->getName(), 'buttons' => $this->getButtons(), ]; } diff --git a/src/Laravel/src/Fields/Relationships/HasMany.php b/src/Laravel/src/Fields/Relationships/HasMany.php index e10bf2369..401fc0d54 100644 --- a/src/Laravel/src/Fields/Relationships/HasMany.php +++ b/src/Laravel/src/Fields/Relationships/HasMany.php @@ -17,6 +17,8 @@ use MoonShine\Contracts\UI\ActionButtonContract; use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Contracts\UI\FieldContract; +use MoonShine\Contracts\UI\FieldWithComponentContract; +use MoonShine\Contracts\UI\FormBuilderContract; use MoonShine\Contracts\UI\HasFieldsContract; use MoonShine\Contracts\UI\TableBuilderContract; use MoonShine\Laravel\Buttons\HasManyButton; @@ -34,8 +36,9 @@ * @template-covariant R of (HasOneOrMany|HasOneOrManyThrough|MorphOneOrMany) * @extends ModelRelationField * @implements HasFieldsContract + * @implements FieldWithComponentContract */ -class HasMany extends ModelRelationField implements HasFieldsContract +class HasMany extends ModelRelationField implements HasFieldsContract, FieldWithComponentContract { use WithFields; use WithRelatedLink; @@ -83,6 +86,8 @@ class HasMany extends ModelRelationField implements HasFieldsContract protected bool $withoutModals = false; + protected null|TableBuilderContract|FormBuilderContract|ActionButtonContract $resolvedComponent = null; + public function withoutModals(): static { $this->withoutModals = true; @@ -559,7 +564,11 @@ protected function resolveValue(): mixed */ public function getComponent(): ComponentContract { - return $this->isRelatedLink() + if (! \is_null($this->resolvedComponent)) { + return $this->resolvedComponent; + } + + return $this->resolvedComponent = $this->isRelatedLink() ? $this->getRelatedLink() : $this->getTableValue(); } diff --git a/src/Laravel/src/Fields/Relationships/HasOne.php b/src/Laravel/src/Fields/Relationships/HasOne.php index 9d5aa03a1..b6b546d9d 100644 --- a/src/Laravel/src/Fields/Relationships/HasOne.php +++ b/src/Laravel/src/Fields/Relationships/HasOne.php @@ -13,6 +13,7 @@ use MoonShine\Contracts\Core\DependencyInjection\FieldsContract; use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Contracts\UI\FieldContract; +use MoonShine\Contracts\UI\FieldWithComponentContract; use MoonShine\Contracts\UI\FormBuilderContract; use MoonShine\Contracts\UI\HasFieldsContract; use MoonShine\Contracts\UI\TableBuilderContract; @@ -32,8 +33,9 @@ * @template-covariant R of HasOneOrMany|HasOneOrManyThrough * @extends ModelRelationField * @implements HasFieldsContract + * @implements FieldWithComponentContract */ -class HasOne extends ModelRelationField implements HasFieldsContract +class HasOne extends ModelRelationField implements HasFieldsContract, FieldWithComponentContract { use WithFields; @@ -57,6 +59,8 @@ class HasOne extends ModelRelationField implements HasFieldsContract protected ?Closure $modifyTable = null; + protected ?FormBuilderContract $resolvedComponent = null; + public function hasWrapper(): bool { return false; @@ -206,8 +210,12 @@ public function modifyTable(Closure $callback): static * @throws Throwable * @throws FieldException */ - protected function getComponent(): FormBuilder + public function getComponent(): ComponentContract { + if (! \is_null($this->resolvedComponent)) { + return $this->resolvedComponent; + } + $resource = $this->getResource()->stopGettingItemFromUrl(); /** @var ?ModelResource $parentResource */ @@ -240,7 +248,7 @@ protected function getComponent(): FormBuilder $isAsync = ! \is_null($item) && ($this->isAsync() || $resource->isAsync()); - return FormBuilder::make($action) + return $this->resolvedComponent = FormBuilder::make($action) ->reactiveUrl( static fn (): string => moonshineRouter() ->getEndpoints() diff --git a/src/Laravel/src/Fields/Relationships/ModelRelationField.php b/src/Laravel/src/Fields/Relationships/ModelRelationField.php index 25c12ced1..0bf701181 100644 --- a/src/Laravel/src/Fields/Relationships/ModelRelationField.php +++ b/src/Laravel/src/Fields/Relationships/ModelRelationField.php @@ -258,6 +258,20 @@ public function getRelatedModel(): ?Model return $this->relatedModel; } + public function makeRelatedModel(int|string $key, array $attributes = [], array $relations = []): ?Model + { + $related = $this->getRelatedModel(); + + if (\is_null($related)) { + return null; + } + + return $related->forceFill([ + $related->getKeyName() => $key, + ...$attributes, + ])->setRelations($relations); + } + /** * @return ?R */ diff --git a/src/Laravel/src/Fields/Relationships/MorphTo.php b/src/Laravel/src/Fields/Relationships/MorphTo.php index d2cc4fdb5..7c74049ad 100644 --- a/src/Laravel/src/Fields/Relationships/MorphTo.php +++ b/src/Laravel/src/Fields/Relationships/MorphTo.php @@ -163,6 +163,10 @@ public function getTypeValue(): string protected function resolveValue(): string { + if (\is_scalar($this->toValue())) { + return $this->toValue(); + } + return (string) $this->getRelatedModel()->{$this->getMorphKey()}; } diff --git a/src/Laravel/src/Fields/Relationships/RelationRepeater.php b/src/Laravel/src/Fields/Relationships/RelationRepeater.php index b3ed9e2df..13e32aa2c 100644 --- a/src/Laravel/src/Fields/Relationships/RelationRepeater.php +++ b/src/Laravel/src/Fields/Relationships/RelationRepeater.php @@ -12,6 +12,7 @@ use MoonShine\Contracts\UI\ActionButtonContract; use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Contracts\UI\FieldContract; +use MoonShine\Contracts\UI\FieldWithComponentContract; use MoonShine\Contracts\UI\HasFieldsContract; use MoonShine\Contracts\UI\TableBuilderContract; use MoonShine\Laravel\Collections\Fields; @@ -25,6 +26,7 @@ use MoonShine\UI\Contracts\HasDefaultValueContract; use MoonShine\UI\Contracts\RemovableContract; use MoonShine\UI\Fields\Field; +use MoonShine\UI\Fields\Json; use MoonShine\UI\Traits\Fields\HasVerticalMode; use MoonShine\UI\Traits\Fields\WithDefaultValue; use MoonShine\UI\Traits\Removable; @@ -35,9 +37,11 @@ /** * @implements HasFieldsContract + * @implements FieldWithComponentContract */ class RelationRepeater extends ModelRelationField implements HasFieldsContract, + FieldWithComponentContract, RemovableContract, HasDefaultValueContract, CanBeArray, @@ -52,8 +56,6 @@ class RelationRepeater extends ModelRelationField implements protected bool $isGroup = true; - protected bool $hasOld = false; - protected bool $resolveValueOnce = true; protected bool $isCreatable = true; @@ -68,6 +70,8 @@ class RelationRepeater extends ModelRelationField implements protected ?Closure $modifyRemoveButton = null; + protected ?TableBuilderContract $resolvedComponent = null; + public function __construct( string|Closure $label, ?string $relationName = null, @@ -223,14 +227,34 @@ protected function resolveValue(): mixed ); } + protected function resolveOldValue(mixed $old): mixed + { + foreach ($this->getFields() as $field) { + if ($field instanceof Json) { + foreach ($old as $index => $value) { + $column = $field->getColumn(); + $old[$index][$column] = $field->prepareOnApplyRecursive( + $value[$column] ?? [] + ); + } + } + } + + return $old; + } + /** * @throws Throwable */ - protected function getComponent(): TableBuilder + public function getComponent(): ComponentContract { + if (! \is_null($this->resolvedComponent)) { + return $this->resolvedComponent; + } + $fields = $this->getPreparedFields(); - return TableBuilder::make($fields, $this->getValue()) + return $this->resolvedComponent = TableBuilder::make($fields, $this->getValue()) ->name("relation_repeater_{$this->getIdentity()}") ->inside('field') ->customAttributes( diff --git a/src/Laravel/src/Traits/Fields/HasTreeMode.php b/src/Laravel/src/Traits/Fields/HasTreeMode.php index 802d5e354..d81331bfa 100644 --- a/src/Laravel/src/Traits/Fields/HasTreeMode.php +++ b/src/Laravel/src/Traits/Fields/HasTreeMode.php @@ -53,13 +53,12 @@ protected function buildTree(Collection $data, int|string $parentKey = 0, int $o foreach ($data->get($parentKey) as $item) { $label = $this->getColumnOrFormattedValue($item, data_get($item, $this->getResourceColumn())); - $this->setAttribute('name', $this->getNameAttribute((string) $item->getKey())); - $element = Checkbox::make($label) ->formName($this->getFormName()) ->simpleMode() ->customAttributes($this->getAttributes()->jsonSerialize()) ->customAttributes($this->getReactiveAttributes()) + ->setNameAttribute($this->getNameAttribute((string) $item->getKey())) ->setValue($item->getKey()); $this->treeHtml .= str((string) $element)->wrap( diff --git a/src/UI/resources/views/fields/relationships/belongs-to-many.blade.php b/src/UI/resources/views/fields/relationships/belongs-to-many.blade.php index 04d662b08..de410881b 100644 --- a/src/UI/resources/views/fields/relationships/belongs-to-many.blade.php +++ b/src/UI/resources/views/fields/relationships/belongs-to-many.blade.php @@ -37,12 +37,12 @@ ])" :nullable="$isNullable" :searchable="$isSearchable" - :values="$value" + :values="$values" :asyncRoute="$isAsyncSearch ? $asyncSearchUrl : null" > @elseif($isTreeMode) -
+
{!! $treeHtml !!}
@else diff --git a/src/UI/resources/views/fields/stack.blade.php b/src/UI/resources/views/fields/stack.blade.php index f61ee1c3a..617d81ba2 100644 --- a/src/UI/resources/views/fields/stack.blade.php +++ b/src/UI/resources/views/fields/stack.blade.php @@ -1,6 +1,8 @@ @props([ 'fields' => [], ]) - +
+ +
diff --git a/src/UI/src/Fields/File.php b/src/UI/src/Fields/File.php index 6d95564d8..72f022024 100644 --- a/src/UI/src/Fields/File.php +++ b/src/UI/src/Fields/File.php @@ -24,6 +24,8 @@ class File extends Field implements FileableContract, RemovableContract protected string $view = 'moonshine::fields.file'; + protected bool $hasOld = false; + protected string $type = 'file'; protected string $accept = '*/*'; diff --git a/src/UI/src/Fields/FormElement.php b/src/UI/src/Fields/FormElement.php index 223aaed53..f783f8917 100644 --- a/src/UI/src/Fields/FormElement.php +++ b/src/UI/src/Fields/FormElement.php @@ -79,6 +79,8 @@ abstract class FormElement extends MoonShineComponent implements FormElementCont protected bool $hasOld = true; + protected bool $isOldValue = false; + protected bool $isGroup = false; protected ComponentAttributesBagContract $wrapperAttributes; @@ -90,19 +92,19 @@ abstract class FormElement extends MoonShineComponent implements FormElementCont public function __construct( Closure|string|null $label = null, ?string $column = null, - ?Closure $formatted = null + ?Closure $formatted = null, ) { parent::__construct(); $this->attributes = new MoonShineComponentAttributeBag( - $this->getPropertyAttributes()->toArray() + $this->getPropertyAttributes()->toArray(), ); $this->wrapperAttributes = new MoonShineComponentAttributeBag(); $this->setLabel($label ?? $this->getLabel()); $this->setColumn( - trim($column ?? str($this->getLabel())->lower()->snake()->value()) + trim($column ?? str($this->getLabel())->lower()->snake()->value()), ); if (! \is_null($formatted)) { @@ -119,7 +121,7 @@ function ($attr): array { return isset($this->{$property}) ? [$attr => $this->{$property}] : []; - } + }, ); } @@ -199,7 +201,7 @@ protected function prepareFill(array $raw = [], ?DataWrapperContract $casted = n return \call_user_func( $this->fillCallback, \is_null($casted) ? $raw : $casted->getOriginal(), - $this + $this, ); } @@ -252,7 +254,7 @@ public function fillData(mixed $value, int $index = 0): static return $this->resolveFill( $casted->toArray(), $casted, - $index + $index, ); } @@ -335,7 +337,11 @@ public function getValue(bool $withOld = true): mixed : $empty; if ($withOld && $old !== $empty) { - return $old; + $this->isOldValue = true; + + $this->setValue( + $this->resolveOldValue($old), + ); } $this->isValueResolved = true; @@ -343,6 +349,16 @@ public function getValue(bool $withOld = true): mixed return $this->resolvedValue = $this->resolveValue(); } + protected function resolveOldValue(mixed $old): mixed + { + return $old; + } + + public function isOldValue(): bool + { + return $this->isOldValue; + } + protected function resolveValue(): mixed { return $this->toValue(); @@ -379,8 +395,8 @@ public function toFormattedValue(): mixed $this->getFormattedValueCallback(), $this->getData()?->getOriginal(), $this->getRowIndex(), - $this - ) + $this, + ), ); } @@ -497,8 +513,8 @@ public function appendRequestKeyPrefix(string $value, ?string $prefix = null): s $this->setRequestKeyPrefix( str($value)->when( $prefix, - static fn ($str) => $str->prepend("$prefix.") - )->value() + static fn ($str) => $str->prepend("$prefix."), + )->value(), ); return $this; @@ -541,8 +557,8 @@ public function getRequestValue(string|int|null $index = null): mixed $value = $this->prepareRequestValue( $this->getCore()->getRequest()->get( $this->getRequestNameDot($index), - $this->getDefaultIfExists() - ) ?? false + $this->getDefaultIfExists(), + ) ?? false, ); if ($this->onRequestValue instanceof Closure) { @@ -564,12 +580,12 @@ public function getRequestNameDot(string|int|null $index = null): string ->when( $this->getRequestKeyPrefix(), fn (Stringable $str): Stringable => $str->prepend( - "{$this->getRequestKeyPrefix()}." - ) + "{$this->getRequestKeyPrefix()}.", + ), ) ->when( ! \is_null($index) && $index !== '', - static fn (Stringable $str): Stringable => $str->append(".$index") + static fn (Stringable $str): Stringable => $str->append(".$index"), )->value(); } diff --git a/src/UI/src/Fields/Hidden.php b/src/UI/src/Fields/Hidden.php index 34b9497ae..b80ec0068 100644 --- a/src/UI/src/Fields/Hidden.php +++ b/src/UI/src/Fields/Hidden.php @@ -17,6 +17,8 @@ class Hidden extends Field implements HasDefaultValueContract, CanBeString protected string $type = 'hidden'; + protected bool $hasOld = false; + protected bool $showValue = false; protected bool $columnSelection = false; diff --git a/src/UI/src/Fields/HiddenIds.php b/src/UI/src/Fields/HiddenIds.php index 2c6974fe9..e336bc4ae 100644 --- a/src/UI/src/Fields/HiddenIds.php +++ b/src/UI/src/Fields/HiddenIds.php @@ -11,6 +11,8 @@ class HiddenIds extends Field { protected string $view = 'moonshine::fields.hidden-ids'; + protected bool $hasOld = false; + protected string $type = 'hidden'; public function __construct( diff --git a/src/UI/src/Fields/Json.php b/src/UI/src/Fields/Json.php index 0582613cc..1edaabcb7 100644 --- a/src/UI/src/Fields/Json.php +++ b/src/UI/src/Fields/Json.php @@ -12,6 +12,7 @@ use MoonShine\Contracts\UI\ComponentAttributesBagContract; use MoonShine\Contracts\UI\ComponentContract; use MoonShine\Contracts\UI\FieldContract; +use MoonShine\Contracts\UI\FieldWithComponentContract; use MoonShine\Contracts\UI\HasFieldsContract; use MoonShine\Contracts\UI\TableBuilderContract; use MoonShine\UI\Collections\Fields; @@ -33,9 +34,11 @@ /** * @implements HasFieldsContract + * @implements FieldWithComponentContract */ class Json extends Field implements HasFieldsContract, + FieldWithComponentContract, RemovableContract, HasDefaultValueContract, CanBeArray @@ -55,8 +58,6 @@ class Json extends Field implements protected bool $isGroup = true; - protected bool $hasOld = false; - protected bool $isCreatable = true; protected ?int $creatableLimit = null; @@ -75,6 +76,8 @@ class Json extends Field implements protected bool $resolveValueOnce = true; + protected null|TableBuilderContract|FieldsGroup $resolvedComponent = null; + /** * @throws Throwable */ @@ -154,7 +157,7 @@ public function isKeyOrOnlyValue(): bool public function creatable( Closure|bool|null $condition = null, ?int $limit = null, - ?ActionButtonContract $button = null + ?ActionButtonContract $button = null, ): static { $this->isCreatable = value($condition, $this) ?? true; @@ -285,15 +288,16 @@ protected function resolveRawValue(): mixed protected function resolvePreview(): Renderable|string { - $value = $this->getValue(); + $component = $this->getComponent(); + // FieldsGroup component if ($this->isObjectMode()) { - return $value + return $component ->previewMode() ->render(); } - return $value + return $component ->simple() ->preview() ->render(); @@ -303,7 +307,7 @@ protected function reformatFilledValue(mixed $data): mixed { if ($this->isKeyOrOnlyValue() && ! $this->isFilterMode()) { return collect($data)->map(fn ($data, $key): array => $this->extractKeyValue( - $this->isOnlyValue() ? [$data] : [$key => $data] + $this->isOnlyValue() ? [$data] : [$key => $data], ))->values()->toArray(); } @@ -340,33 +344,63 @@ protected function isBlankValue(): bool /** * @throws Throwable */ - protected function resolveValue(): mixed + public function prepareOnApplyRecursive(iterable $collection): array + { + $collection = $this->prepareOnApply($collection); + + foreach ($this->getFields() as $field) { + if ($field instanceof self) { + foreach ($collection as $index => $value) { + $column = $field->getColumn(); + $collection[$index][$column] = $field->prepareOnApplyRecursive( + $value[$column] ?? [] + ); + } + } + } + + return $collection; + } + + /** + * @throws Throwable + */ + protected function resolveOldValue(mixed $old): mixed + { + return $this->prepareOnApplyRecursive($old); + } + + public function getComponent(): ComponentContract { + if (! \is_null($this->resolvedComponent)) { + return $this->resolvedComponent; + } + $value = $this->isPreviewMode() ? $this->toFormattedValue() - : $this->toValue(); + : $this->getValue(); $values = Collection::make( is_iterable($value) ? $value - : [] + : [], ); $fields = $this->getPreparedFields(); if ($this->isObjectMode()) { return FieldsGroup::make( - $fields + $fields, )->fill($values->toArray())->mapFields( fn (FieldContract $field): FieldContract => $field ->formName($this->getFormName()) - ->setParent($this) + ->setParent($this), ); } $values = $values->when( ! $this->isPreviewMode() && ! $this->isCreatable() && blank($values), - static fn ($values): Collection => $values->push([null]) + static fn ($values): Collection => $values->push([null]), ); $reorderable = ! $this->isPreviewMode() && $this->isReorderable(); @@ -375,12 +409,12 @@ protected function resolveValue(): mixed $fields->prepend( Preview::make( column: '__handle', - formatted: static fn () => Icon::make('bars-4') - )->customAttributes(['class' => 'handle', 'style' => 'cursor: move']) + formatted: static fn () => Icon::make('bars-4'), + )->customAttributes(['class' => 'handle', 'style' => 'cursor: move']), ); } - return TableBuilder::make($fields, $values) + $component = TableBuilder::make($fields, $values) ->name('repeater_' . $this->getColumn()) ->inside('field') ->customAttributes( @@ -390,13 +424,13 @@ protected function resolveValue(): mixed $reorderable, static fn (ComponentAttributesBagContract $attr): ComponentAttributesBagContract => $attr->merge([ 'data-handle' => '.handle', - ]) + ]), ) - ->jsonSerialize() + ->jsonSerialize(), ) ->when( $reorderable, - static fn (TableBuilderContract $table): TableBuilderContract => $table->reorderable() + static fn (TableBuilderContract $table): TableBuilderContract => $table->reorderable(), ) ->when( $this->isVertical(), @@ -411,18 +445,35 @@ protected function resolveValue(): mixed /** @var Column $default */ /** @phpstan-ignore-next-line */ : $default->columnSpan($this->verticalValueSpan) : null, - ) + ), ) ->when( ! \is_null($this->modifyTable), - fn (TableBuilder $tableBuilder) => value($this->modifyTable, $tableBuilder, $this->isPreviewMode()) + fn (TableBuilder $tableBuilder) => value($this->modifyTable, $tableBuilder, $this->isPreviewMode()), ); + + if (! $this->isPreviewMode()) { + $component = $component + ->editable() + ->reindex(prepared: true) + ->when( + $this->isCreatable(), + fn (TableBuilderContract $table): TableBuilderContract => $table->creatable( + limit: $this->getCreateLimit(), + button: $this->getCreateButton(), + )->removeAfterClone(), + ) + ->buttons($this->getButtons()) + ->simple(); + } + + return $this->resolvedComponent = $component; } /** * @throws Throwable */ - protected function prepareOnApply(iterable $collection): array + public function prepareOnApply(iterable $collection): array { $collection = collect($collection); @@ -431,8 +482,8 @@ protected function prepareOnApply(iterable $collection): array fn ($data): Collection => $data->mapWithKeys( fn ($data, $key): array => $this->isOnlyValue() ? [$key => $data['value']] - : [$data['key'] => $data['value']] - ) + : [$data['key'] => $data['value']], + ), )->filter(fn ($value): bool => $this->filterEmpty($value))->toArray(); } @@ -454,7 +505,7 @@ protected function resolveAppliesCallback( mixed $data, Closure $callback, ?Closure $response = null, - bool $fill = false + bool $fill = false, ): mixed { $requestValues = array_filter($this->getRequestValue() ?: []); $applyValues = []; @@ -481,7 +532,7 @@ protected function resolveAppliesCallback( /** @phpstan-ignore-next-line */ $applyValues[$index], $field->getColumn(), - data_get($apply, $field->getColumn()) + data_get($apply, $field->getColumn()), ); } } @@ -496,7 +547,7 @@ protected function resolveAppliesCallback( return \is_null($response) ? data_set( $data, str_replace('.', '->', $this->getColumn()), - $values + $values, ) : $response($values, $data); } @@ -506,7 +557,7 @@ protected function resolveOnApply(): ?Closure data: $item, callback: static fn (FieldContract $field, mixed $values): mixed => $field->apply( static fn ($data): mixed => data_set($data, $field->getColumn(), data_get($values, $field->getColumn(), '')), - $values + $values, ), ); } @@ -548,7 +599,7 @@ protected function resolveAfterDestroy(mixed $data): mixed ->each( static fn (FieldContract $field): mixed => $field ->fillData($value) - ->afterDestroy($value) + ->afterDestroy($value), ); } } @@ -573,28 +624,8 @@ public function getReactiveValue(): mixed */ protected function viewData(): array { - /** @var TableBuilderContract $value */ - $value = $this->getValue(); - - if ($this->isObjectMode()) { - return [ - 'component' => $value, - ]; - } - return [ - 'component' => $value - ->editable() - ->reindex(prepared: true) - ->when( - $this->isCreatable(), - fn (TableBuilderContract $table): TableBuilderContract => $table->creatable( - limit: $this->getCreateLimit(), - button: $this->getCreateButton() - )->removeAfterClone() - ) - ->buttons($this->getButtons()) - ->simple(), + 'component' => $this->getComponent(), ]; } } diff --git a/src/UI/src/Fields/Password.php b/src/UI/src/Fields/Password.php index df0dfb5ee..f52517174 100644 --- a/src/UI/src/Fields/Password.php +++ b/src/UI/src/Fields/Password.php @@ -11,6 +11,8 @@ class Password extends Text { protected string $type = 'password'; + protected bool $hasOld = false; + protected function resolvePreview(): string { return '***'; diff --git a/src/UI/src/Fields/Template.php b/src/UI/src/Fields/Template.php index 817415a8c..bd8ce8e63 100644 --- a/src/UI/src/Fields/Template.php +++ b/src/UI/src/Fields/Template.php @@ -20,6 +20,8 @@ class Template extends Field implements HasFieldsContract { use WithFields; + protected bool $hasOld = false; + protected function prepareFields(): FieldsContract { return tap( diff --git a/src/UI/src/Traits/Fields/WithQuickFormElementAttributes.php b/src/UI/src/Traits/Fields/WithQuickFormElementAttributes.php index e6652c8ba..76dd7fe4a 100644 --- a/src/UI/src/Traits/Fields/WithQuickFormElementAttributes.php +++ b/src/UI/src/Traits/Fields/WithQuickFormElementAttributes.php @@ -22,7 +22,7 @@ public function setNameAttribute(string $name): static { $this->nameAttribute = $name; - return $this; + return $this->setAttribute('name', $name); } public function getNameAttribute(?string $index = null): string