From a07002fe621daaf31bea6f3e9a8e910be99a20a5 Mon Sep 17 00:00:00 2001 From: LT Date: Sat, 30 Nov 2024 11:02:09 +0300 Subject: [PATCH 1/6] feat: HasMany/BelongsToMany Improve preview/form modes Improve relatedLink --- .../src/Fields/Relationships/HasMany.php | 41 +++++++------- .../src/Traits/Fields/WithRelatedLink.php | 24 +++++---- .../Traits/Resource/ResourceModelQuery.php | 18 +++++++ .../Fields/Relationships/HasManyFieldTest.php | 54 +++++++++++++++---- 4 files changed, 95 insertions(+), 42 deletions(-) diff --git a/src/Laravel/src/Fields/Relationships/HasMany.php b/src/Laravel/src/Fields/Relationships/HasMany.php index a1581d1d3..8ab125003 100644 --- a/src/Laravel/src/Fields/Relationships/HasMany.php +++ b/src/Laravel/src/Fields/Relationships/HasMany.php @@ -5,6 +5,7 @@ namespace MoonShine\Laravel\Fields\Relationships; use Closure; +use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Contracts\Support\Renderable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasOneOrMany; @@ -20,6 +21,7 @@ use MoonShine\Contracts\UI\TableBuilderContract; use MoonShine\Laravel\Buttons\HasManyButton; use MoonShine\Laravel\Collections\Fields; +use MoonShine\Laravel\Resources\ModelResource; use MoonShine\Laravel\Traits\Fields\WithRelatedLink; use MoonShine\UI\Components\ActionGroup; use MoonShine\UI\Components\Table\TableBuilder; @@ -340,13 +342,23 @@ public function prepareClonedFields(): FieldsContract */ protected function getTablePreview(): TableBuilderContract { - $items = $this->toValue(); + /** @var ModelResource $resource */ + $resource = clone $this->getResource() + ->disableSaveQueryState(); - if (filled($items)) { - $items = $items->take($this->getLimit()); - } + $resource->disableQueryFeatures(); + + $casted = $this->getRelatedModel(); + $relation = $casted?->{$this->getRelationName()}(); + + /** @var Builder $query */ + $query = \is_null($this->modifyBuilder) + ? $relation + : value($this->modifyBuilder, $relation); + + $resource->customQueryBuilder($query->limit($this->getLimit())); - $resource = $this->getResource(); + $items = $resource->getQuery()->get(); return TableBuilder::make(items: $items) ->fields($this->getFieldsOnPreview()) @@ -490,7 +502,7 @@ protected function prepareFill(array $raw = [], ?DataWrapperContract $casted = n protected function resolveRawValue(): mixed { - return collect($this->toValue()) + return $this->toRelatedCollection() ->map(fn (Model $item) => data_get($item, $this->getResourceColumn())) ->implode(';'); } @@ -500,12 +512,6 @@ protected function resolveRawValue(): mixed */ protected function resolvePreview(): Renderable|string { - // resolve value before call toValue - if (\is_null($this->toValue())) { - $casted = $this->getRelatedModel(); - $this->setValue($casted?->{$this->getRelationName()}); - } - return $this->isRelatedLink() ? $this->getRelatedLink()->render() : $this->getTablePreview()->render(); @@ -532,11 +538,7 @@ protected function resolveValue(): mixed : value($this->modifyBuilder, $relation) ); - $items = $resource->getItems(); - - $this->setValue($items); - - return $items; + return $resource->getItems(); } /** @@ -544,11 +546,6 @@ protected function resolveValue(): mixed */ public function getComponent(): ComponentContract { - // resolve value before call toValue - if (\is_null($this->toValue())) { - $this->setValue($this->getValue()); - } - return $this->isRelatedLink() ? $this->getRelatedLink() : $this->getTableValue(); diff --git a/src/Laravel/src/Traits/Fields/WithRelatedLink.php b/src/Laravel/src/Traits/Fields/WithRelatedLink.php index b123b366a..1f1af950a 100644 --- a/src/Laravel/src/Traits/Fields/WithRelatedLink.php +++ b/src/Laravel/src/Traits/Fields/WithRelatedLink.php @@ -37,16 +37,20 @@ public function relatedLink(?string $linkRelation = null, Closure|bool|null $con return $this; } + public function toRelatedCollection(): Collection + { + return $this->getRelatedModel()?->{$this->getRelationName()} ?? new Collection(); + } + protected function isRelatedLink(): bool { - if (\is_callable($this->isRelatedLink) && \is_null($this->toValue())) { - return (bool) value($this->isRelatedLink, 0, $this); + if($this->isRelatedLink === false) { + return false; } if (\is_callable($this->isRelatedLink)) { - $count = $this->toValue() instanceof Collection - ? $this->toValue()->count() - : $this->toValue()->total(); + $value = $this->toRelatedCollection(); + $count = $value->count(); return (bool) value($this->isRelatedLink, $count, $this); } @@ -75,22 +79,20 @@ protected function getRelatedLink(bool $preview = false): ActionButtonContract { $relationName = $this->getRelatedLinkRelation(); - $value = $this->toValue(); - $count = $value instanceof LengthAwarePaginator - ? $value->total() - : $value->count(); + $value = $this->toRelatedCollection(); + $count = $value->count(); return ActionButton::make( '', url: $this->getResource()->getIndexPageUrl([ '_parentId' => $relationName . '-' . $this->getRelatedModel()?->getKey(), - ]) + ]), ) ->badge($count) ->icon('eye') ->when( ! \is_null($this->modifyRelatedLink), - fn (ActionButtonContract $button) => value($this->modifyRelatedLink, $button, $preview) + fn (ActionButtonContract $button) => value($this->modifyRelatedLink, $button, $preview), ); } diff --git a/src/Laravel/src/Traits/Resource/ResourceModelQuery.php b/src/Laravel/src/Traits/Resource/ResourceModelQuery.php index 0eb2df082..766cb8972 100644 --- a/src/Laravel/src/Traits/Resource/ResourceModelQuery.php +++ b/src/Laravel/src/Traits/Resource/ResourceModelQuery.php @@ -33,6 +33,8 @@ trait ResourceModelQuery protected ?Builder $customQueryBuilder = null; + protected bool $disableQueryFeatures = false; + /** * @throws Throwable */ @@ -138,6 +140,10 @@ public function customQueryBuilder(Builder $builder): static */ protected function queryBuilderFeatures(): void { + if($this->isDisabledQueryFeatures()) { + return; + } + $this ->withCache() ->withTags() @@ -148,6 +154,18 @@ protected function queryBuilderFeatures(): void ->withCachedQueryParams(); } + public function isDisabledQueryFeatures(): bool + { + return $this->disableQueryFeatures; + } + + public function disableQueryFeatures(): static + { + $this->disableQueryFeatures = true; + + return $this; + } + public function isItemExists(): bool { return ! \is_null($this->getItem()) && $this->getItem()->exists; diff --git a/tests/Feature/Fields/Relationships/HasManyFieldTest.php b/tests/Feature/Fields/Relationships/HasManyFieldTest.php index f88a171ba..6f7f1dd1e 100644 --- a/tests/Feature/Fields/Relationships/HasManyFieldTest.php +++ b/tests/Feature/Fields/Relationships/HasManyFieldTest.php @@ -5,17 +5,18 @@ uses()->group('model-relation-fields'); uses()->group('has-many-field'); +use Illuminate\Database\Eloquent\Relations\Relation; use MoonShine\Laravel\Fields\Relationships\HasMany; use MoonShine\Laravel\Pages\Crud\FormPage; use MoonShine\Laravel\Pages\Crud\IndexPage; +use MoonShine\Tests\Fixtures\Models\Comment; use MoonShine\Tests\Fixtures\Models\Item; use MoonShine\Tests\Fixtures\Resources\TestCommentResource; use MoonShine\Tests\Fixtures\Resources\TestResourceBuilder; -use MoonShine\UI\Fields\Field; use MoonShine\UI\Fields\ID; use MoonShine\UI\Fields\Text; -it('onlyLink preview', function () { +it('relatedLink preview', function () { createItem(countComments: 6); $resource = TestResourceBuilder::new(Item::class)->setTestFields([ @@ -31,7 +32,7 @@ ; }); -it('onlyLink preview empty', function () { +it('relatedLink preview empty', function () { createItem(countComments: 0); $resource = TestResourceBuilder::new(Item::class)->setTestFields([ @@ -47,7 +48,7 @@ ; }); -it('onlyLink value', function () { +it('relatedLink value', function () { $item = createItem(countComments: 16); $resource = TestResourceBuilder::new(Item::class)->setTestFields([ @@ -64,7 +65,7 @@ ; }); -it('onlyLink value empty', function () { +it('relatedLink value empty', function () { $item = createItem(countComments: 0); $resource = TestResourceBuilder::new(Item::class)->setTestFields([ @@ -80,7 +81,7 @@ ; }); -it('onlyLink preview condition', function () { +it('relatedLink preview condition', function () { $item = createItem(countComments: 6); $resource = TestResourceBuilder::new(Item::class)->setTestFields([ @@ -102,15 +103,15 @@ ; }); -it('onlyLink value condition', function () { +it('relatedLink value condition', function () { $item = createItem(countComments: 16); $resource = TestResourceBuilder::new(Item::class)->setTestFields([ ID::make(), Text::make('Name'), HasMany::make('Comments title', 'comments', resource: TestCommentResource::class) - ->relatedLink(condition: static function (int $count, Field $field): bool { - return $field->toValue()->total() > 20; + ->relatedLink(condition: static function (int $count, HasMany $field): bool { + return $field->toRelatedCollection()->count() > 20; }) , ]); @@ -163,3 +164,38 @@ expect($hasMany->getResource()->getItemID()) ->toBeNull(); }); + +it('modify builder', function () { + $item = createItem(countComments: 2); + + $comments = Comment::query()->get(); + + $commentFirst = $comments->first(); + $commentLast = $comments->last(); + + $resource = TestResourceBuilder::new(Item::class)->setTestFields([ + ID::make(), + Text::make('Name'), + HasMany::make('Comments title', 'comments', resource: TestCommentResource::class) + ->modifyBuilder( + fn (Relation $relation) => $relation->where('id', $commentFirst->id) + ) + , + ]); + + asAdmin() + ->get($this->moonshineCore->getRouter()->getEndpoints()->toPage(page: IndexPage::class, resource: $resource)) + ->assertOk() + ->assertSee('Comments title') + ->assertSee($commentFirst->content) + ->assertDontSee($commentLast->content) + ; + + asAdmin() + ->get($this->moonshineCore->getRouter()->getEndpoints()->toPage(page: FormPage::class, resource: $resource, params: ['resourceItem' => $item->id])) + ->assertOk() + ->assertSee('Comments title') + ->assertSee($commentFirst->content) + ->assertDontSee($commentLast->content) + ; +}); From a1195453ab6ea8ac103be77198616078bbad9310 Mon Sep 17 00:00:00 2001 From: lee-to Date: Sat, 30 Nov 2024 08:03:13 +0000 Subject: [PATCH 2/6] [rector] Rector fixes --- src/Laravel/src/Traits/Fields/WithRelatedLink.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Laravel/src/Traits/Fields/WithRelatedLink.php b/src/Laravel/src/Traits/Fields/WithRelatedLink.php index 1f1af950a..2f4963e7e 100644 --- a/src/Laravel/src/Traits/Fields/WithRelatedLink.php +++ b/src/Laravel/src/Traits/Fields/WithRelatedLink.php @@ -5,7 +5,6 @@ namespace MoonShine\Laravel\Traits\Fields; use Closure; -use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use MoonShine\Contracts\UI\ActionButtonContract; use MoonShine\Laravel\Fields\Relationships\BelongsToMany; From fcbe8c95d74658cb93b2623057690c4a4a5ca9a4 Mon Sep 17 00:00:00 2001 From: lee-to Date: Sat, 30 Nov 2024 08:03:35 +0000 Subject: [PATCH 3/6] Fix styling --- src/Laravel/src/Traits/Fields/WithRelatedLink.php | 2 +- src/Laravel/src/Traits/Resource/ResourceModelQuery.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Laravel/src/Traits/Fields/WithRelatedLink.php b/src/Laravel/src/Traits/Fields/WithRelatedLink.php index 2f4963e7e..6b82282fb 100644 --- a/src/Laravel/src/Traits/Fields/WithRelatedLink.php +++ b/src/Laravel/src/Traits/Fields/WithRelatedLink.php @@ -43,7 +43,7 @@ public function toRelatedCollection(): Collection protected function isRelatedLink(): bool { - if($this->isRelatedLink === false) { + if ($this->isRelatedLink === false) { return false; } diff --git a/src/Laravel/src/Traits/Resource/ResourceModelQuery.php b/src/Laravel/src/Traits/Resource/ResourceModelQuery.php index 766cb8972..920dde9b8 100644 --- a/src/Laravel/src/Traits/Resource/ResourceModelQuery.php +++ b/src/Laravel/src/Traits/Resource/ResourceModelQuery.php @@ -140,7 +140,7 @@ public function customQueryBuilder(Builder $builder): static */ protected function queryBuilderFeatures(): void { - if($this->isDisabledQueryFeatures()) { + if ($this->isDisabledQueryFeatures()) { return; } From 59fa6a5c6e5959d7efb6edd5e5c348b45357520b Mon Sep 17 00:00:00 2001 From: LT Date: Sat, 30 Nov 2024 15:59:53 +0300 Subject: [PATCH 4/6] fix: HasMany --- .../src/Fields/Relationships/HasMany.php | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Laravel/src/Fields/Relationships/HasMany.php b/src/Laravel/src/Fields/Relationships/HasMany.php index 8ab125003..5c0640774 100644 --- a/src/Laravel/src/Fields/Relationships/HasMany.php +++ b/src/Laravel/src/Fields/Relationships/HasMany.php @@ -346,19 +346,23 @@ protected function getTablePreview(): TableBuilderContract $resource = clone $this->getResource() ->disableSaveQueryState(); - $resource->disableQueryFeatures(); + if(\is_null($this->modifyBuilder) && $this->getRelatedModel()?->relationLoaded($this->getRelationName()) === true) { + $items = $this->toRelatedCollection(); + } else { + $resource->disableQueryFeatures(); - $casted = $this->getRelatedModel(); - $relation = $casted?->{$this->getRelationName()}(); + $casted = $this->getRelatedModel(); + $relation = $casted?->{$this->getRelationName()}(); - /** @var Builder $query */ - $query = \is_null($this->modifyBuilder) - ? $relation - : value($this->modifyBuilder, $relation); + /** @var Builder $query */ + $query = \is_null($this->modifyBuilder) + ? $relation + : value($this->modifyBuilder, $relation); - $resource->customQueryBuilder($query->limit($this->getLimit())); + $resource->customQueryBuilder($query->limit($this->getLimit())); - $items = $resource->getQuery()->get(); + $items = $resource->getQuery()->get(); + } return TableBuilder::make(items: $items) ->fields($this->getFieldsOnPreview()) From f20759d118ef7bff390738953bb2f91de5af018a Mon Sep 17 00:00:00 2001 From: lee-to Date: Sat, 30 Nov 2024 13:00:24 +0000 Subject: [PATCH 5/6] Fix styling --- src/Laravel/src/Fields/Relationships/HasMany.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Laravel/src/Fields/Relationships/HasMany.php b/src/Laravel/src/Fields/Relationships/HasMany.php index 5c0640774..cd8b4fe6c 100644 --- a/src/Laravel/src/Fields/Relationships/HasMany.php +++ b/src/Laravel/src/Fields/Relationships/HasMany.php @@ -346,7 +346,7 @@ protected function getTablePreview(): TableBuilderContract $resource = clone $this->getResource() ->disableSaveQueryState(); - if(\is_null($this->modifyBuilder) && $this->getRelatedModel()?->relationLoaded($this->getRelationName()) === true) { + if (\is_null($this->modifyBuilder) && $this->getRelatedModel()?->relationLoaded($this->getRelationName()) === true) { $items = $this->toRelatedCollection(); } else { $resource->disableQueryFeatures(); From 26546a068f65c317eed964d69f5784898b85d212 Mon Sep 17 00:00:00 2001 From: LT Date: Sun, 1 Dec 2024 22:00:18 +0300 Subject: [PATCH 6/6] fix: Comment --- src/Laravel/src/Fields/Relationships/HasMany.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Laravel/src/Fields/Relationships/HasMany.php b/src/Laravel/src/Fields/Relationships/HasMany.php index 5c0640774..b20c10365 100644 --- a/src/Laravel/src/Fields/Relationships/HasMany.php +++ b/src/Laravel/src/Fields/Relationships/HasMany.php @@ -346,6 +346,7 @@ protected function getTablePreview(): TableBuilderContract $resource = clone $this->getResource() ->disableSaveQueryState(); + // If the records are already in memory (eager load) and there is no modifier, then we take the records from memory if(\is_null($this->modifyBuilder) && $this->getRelatedModel()?->relationLoaded($this->getRelationName()) === true) { $items = $this->toRelatedCollection(); } else {