diff --git a/src/Actions/DuplicateModel.php b/src/Actions/DuplicateModel.php index 7381774a..ad602164 100644 --- a/src/Actions/DuplicateModel.php +++ b/src/Actions/DuplicateModel.php @@ -71,8 +71,8 @@ private function duplicateModel(Model $original, Resource $resource): Model { $model = $original->replicate($resource->blueprint()->fields()->all()->reject->shouldBeDuplicated()->keys()->all()); - if ($resource->titleField() && in_array($resource->titleField(), $resource->databaseColumns())) { - $model->setAttribute($resource->titleField(), $original->getAttribute($resource->titleField()).' (Duplicate)'); + if ($resource->titleField()) { + $resource->setTitleValue($model, $resource->titleValue($original).' (Duplicate)'); } if ($resource->hasPublishStates()) { diff --git a/src/Fieldtypes/BaseFieldtype.php b/src/Fieldtypes/BaseFieldtype.php index dc72a32a..124979bb 100644 --- a/src/Fieldtypes/BaseFieldtype.php +++ b/src/Fieldtypes/BaseFieldtype.php @@ -204,7 +204,7 @@ private function applyOrderingToIndexQuery(Builder|SearchQueryBuilder $query, Fi if ($orderBy = OrderBy::column($request->input('sort'))) { // The stack selector always uses `title` as the default sort column, but // the "title field" for the model might be a different column, so we need to convert it. - $sortColumn = $orderBy === 'title' ? $this->resource()->titleField() : $orderBy; + $sortColumn = $orderBy === 'title' ? $this->resource()->titleDbColumn() : $orderBy; $query->reorder($sortColumn, $request->input('order')); } @@ -304,7 +304,7 @@ public function preProcessIndex($data) return [ 'id' => $model->{$resource->primaryKey()}, - 'title' => $fieldtype->preProcessIndex($model->{$column}), + 'title' => $fieldtype->preProcessIndex($resource->titleValue($model)), 'edit_url' => $model->runwayEditUrl(), ]; }); diff --git a/src/Http/Controllers/CP/ModelRevisionsController.php b/src/Http/Controllers/CP/ModelRevisionsController.php index 72bf5d56..ff1a0856 100644 --- a/src/Http/Controllers/CP/ModelRevisionsController.php +++ b/src/Http/Controllers/CP/ModelRevisionsController.php @@ -60,7 +60,7 @@ public function show(Request $request, Resource $resource, Model $model, $revisi [$values, $meta] = $this->extractFromFields($model, $resource, $blueprint); return [ - 'title' => $model->getAttribute($resource->titleField()), + 'title' => $resource->titleValue($model), 'editing' => true, 'actions' => [ 'save' => $model->runwayUpdateUrl(), diff --git a/src/Http/Controllers/CP/ResourceController.php b/src/Http/Controllers/CP/ResourceController.php index ca9f28f9..bd7f175a 100644 --- a/src/Http/Controllers/CP/ResourceController.php +++ b/src/Http/Controllers/CP/ResourceController.php @@ -142,7 +142,7 @@ public function edit(EditRequest $request, Resource $resource, Model $model) [$values, $meta] = $this->extractFromFields($model, $resource, $blueprint); $viewData = [ - 'title' => $model->getAttribute($resource->titleField()), + 'title' => $resource->titleValue($model), 'reference' => $model->reference(), 'method' => 'patch', 'resource' => $resource->toArray(), diff --git a/src/Http/Controllers/CP/Traits/ExtractsFromModelFields.php b/src/Http/Controllers/CP/Traits/ExtractsFromModelFields.php index 0d727581..232a76e3 100644 --- a/src/Http/Controllers/CP/Traits/ExtractsFromModelFields.php +++ b/src/Http/Controllers/CP/Traits/ExtractsFromModelFields.php @@ -22,7 +22,7 @@ protected function extractFromFields(Model $model, Resource $resource, Blueprint $values = $fields->values()->merge([ 'id' => $model->getKey(), - 'title' => $model->getAttribute($resource->titleField()), + 'title' => $resource->titleValue($model), 'edit_url' => $model->runwayEditUrl(), $resource->publishedColumn() => $model->published(), ]); diff --git a/src/Http/Resources/CP/ListedModel.php b/src/Http/Resources/CP/ListedModel.php index 9f273365..3792d788 100644 --- a/src/Http/Resources/CP/ListedModel.php +++ b/src/Http/Resources/CP/ListedModel.php @@ -44,7 +44,7 @@ public function toArray($request): array return [ 'id' => $model->getKey(), - 'title' => $model->getAttribute($this->runwayResource->titleField()), + 'title' => $this->runwayResource->titleValue($model), 'published' => $this->resource->published(), 'status' => $this->resource->publishedStatus(), 'edit_url' => $model->runwayEditUrl(), diff --git a/src/Http/Resources/CP/Model.php b/src/Http/Resources/CP/Model.php index b1b0bef8..aae98252 100644 --- a/src/Http/Resources/CP/Model.php +++ b/src/Http/Resources/CP/Model.php @@ -13,7 +13,7 @@ public function toArray($request) $data = [ 'id' => $this->resource->getKey(), 'reference' => $this->resource->reference(), - 'title' => $this->resource->{$runwayResource->titleField()}, + 'title' => $runwayResource->titleValue($this->resource), 'permalink' => $runwayResource->hasRouting() ? $this->resource->absoluteUrl() : null, 'status' => $this->resource->publishedStatus(), 'published' => $this->resource->published(), diff --git a/src/Resource.php b/src/Resource.php index 9958c810..ed12e098 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -163,6 +163,67 @@ public function titleField() ->first(); } + public function titleValue(Model $model): mixed + { + $titleField = $this->titleField(); + + if (! $titleField) { + return null; + } + + if ($prefix = $this->nestedFieldPrefix($titleField)) { + $key = Str::after($titleField, "{$prefix}_"); + + return data_get($model, "{$prefix}.{$key}"); + } + + return $model->getAttribute($titleField); + } + + public function titleDbColumn(): ?string + { + $titleField = $this->titleField(); + + if (! $titleField) { + return null; + } + + if ($prefix = $this->nestedFieldPrefix($titleField)) { + $key = Str::after($titleField, "{$prefix}_"); + + return "{$prefix}->{$key}"; + } + + return $titleField; + } + + public function setTitleValue(Model $model, mixed $value): void + { + $titleField = $this->titleField(); + + if (! $titleField) { + return; + } + + if ($prefix = $this->nestedFieldPrefix($titleField)) { + $key = Str::after($titleField, "{$prefix}_"); + $current = $model->getAttribute($prefix) ?? []; + + if (is_object($current)) { + $current = (array) $current; + } + + data_set($current, $key, $value); + $model->setAttribute($prefix, $current); + + return; + } + + if (in_array($titleField, $this->databaseColumns())) { + $model->setAttribute($titleField, $value); + } + } + public function icon(): string { $navItem = Nav::build()->pluck('items')->flatten() diff --git a/src/Search/Searchable.php b/src/Search/Searchable.php index 96e4ad9d..164f0783 100644 --- a/src/Search/Searchable.php +++ b/src/Search/Searchable.php @@ -80,7 +80,7 @@ public function toSearchResult(): Result public function getCpSearchResultTitle(): string { - return $this->model->{$this->resource->titleField()}; + return $this->resource->titleValue($this->model); } public function getCpSearchResultUrl(): string diff --git a/tests/Actions/DuplicateModelTest.php b/tests/Actions/DuplicateModelTest.php index 0428cced..d5ea6157 100644 --- a/tests/Actions/DuplicateModelTest.php +++ b/tests/Actions/DuplicateModelTest.php @@ -194,6 +194,26 @@ public function only_duplicates_fields_where_duplication_is_enabled() $this->assertFalse($duplicate->published()); } + #[Test] + public function it_duplicates_models_with_nested_title_field() + { + Config::set('runway.resources.StatamicRadPack\Runway\Tests\Fixtures\Models\Post.title_field', 'values_alt_title'); + + Runway::discoverResources(); + + $post = Post::factory()->create([ + 'values' => ['alt_title' => 'Nested Hello', 'alt_body' => 'Some body'], + ]); + + (new DuplicateModel)->run(collect([$post]), []); + + $duplicate = Post::query()->whereNot('id', $post->id)->first(); + + $this->assertEquals('Nested Hello (Duplicate)', data_get($duplicate, 'values.alt_title')); + $this->assertEquals('Some body', data_get($duplicate, 'values.alt_body')); + $this->assertFalse($duplicate->published()); + } + #[Test] public function it_does_not_duplicate_the_title_field_when_it_is_computed() { diff --git a/tests/Http/Controllers/CP/ResourceListingControllerTest.php b/tests/Http/Controllers/CP/ResourceListingControllerTest.php index e422c344..0a40d099 100644 --- a/tests/Http/Controllers/CP/ResourceListingControllerTest.php +++ b/tests/Http/Controllers/CP/ResourceListingControllerTest.php @@ -53,6 +53,36 @@ public function can_sort_listing_rows() ]); } + #[Test] + public function can_sort_listing_rows_with_nested_title_field() + { + Config::set('runway.resources.StatamicRadPack\Runway\Tests\Fixtures\Models\Post.title_field', 'values_alt_title'); + + Runway::discoverResources(); + + $user = User::make()->makeSuper()->save(); + + $postA = Post::factory()->create(['values' => ['alt_title' => 'Nested Alpha']]); + $postB = Post::factory()->create(['values' => ['alt_title' => 'Nested Beta']]); + + $this + ->actingAs($user) + ->get(cp_route('runway.listing-api', ['resource' => 'post'])) + ->assertOk() + ->assertJson([ + 'data' => [ + [ + 'title' => 'Nested Alpha', + 'id' => $postA->id, + ], + [ + 'title' => 'Nested Beta', + 'id' => $postB->id, + ], + ], + ]); + } + #[Test] public function listing_rows_are_ordered_as_per_config() { diff --git a/tests/ResourceTest.php b/tests/ResourceTest.php index cc9ce98b..99205663 100644 --- a/tests/ResourceTest.php +++ b/tests/ResourceTest.php @@ -7,6 +7,7 @@ use Statamic\Facades\Blueprint; use Statamic\Facades\Fieldset; use StatamicRadPack\Runway\Runway; +use StatamicRadPack\Runway\Tests\Fixtures\Models\Post; class ResourceTest extends TestCase { @@ -274,6 +275,102 @@ public function can_get_title_field() $this->assertEquals('values->listable_hidden_field', $resource->titleField()); } + #[Test] + public function can_get_title_value() + { + Runway::discoverResources(); + + $resource = Runway::findResource('post'); + + $post = Post::factory()->create(['title' => 'Hello World']); + + $this->assertEquals('Hello World', $resource->titleValue($post)); + } + + #[Test] + public function can_get_title_value_for_nested_field() + { + Config::set('runway.resources.StatamicRadPack\Runway\Tests\Fixtures\Models\Post.title_field', 'values_alt_title'); + + Runway::discoverResources(); + + $resource = Runway::findResource('post'); + + $post = Post::factory()->create(['values' => ['alt_title' => 'Nested Title']]); + + $this->assertEquals('Nested Title', $resource->titleValue($post)); + } + + #[Test] + public function can_get_title_db_column() + { + Runway::discoverResources(); + + $resource = Runway::findResource('post'); + + $this->assertEquals('title', $resource->titleDbColumn()); + } + + #[Test] + public function can_get_title_db_column_for_nested_field() + { + Config::set('runway.resources.StatamicRadPack\Runway\Tests\Fixtures\Models\Post.title_field', 'values_alt_title'); + + Runway::discoverResources(); + + $resource = Runway::findResource('post'); + + $this->assertEquals('values->alt_title', $resource->titleDbColumn()); + } + + #[Test] + public function can_set_title_value() + { + Runway::discoverResources(); + + $resource = Runway::findResource('post'); + + $post = Post::factory()->create(['title' => 'Original']); + + $resource->setTitleValue($post, 'Updated'); + + $this->assertEquals('Updated', $post->getAttribute('title')); + } + + #[Test] + public function can_set_title_value_for_nested_field() + { + Config::set('runway.resources.StatamicRadPack\Runway\Tests\Fixtures\Models\Post.title_field', 'values_alt_title'); + + Runway::discoverResources(); + + $resource = Runway::findResource('post'); + + $post = Post::factory()->create(['values' => ['alt_title' => 'Original', 'alt_body' => 'Keep me']]); + + $resource->setTitleValue($post, 'Updated'); + + $this->assertEquals('Updated', data_get($post, 'values.alt_title')); + $this->assertEquals('Keep me', data_get($post, 'values.alt_body')); + } + + #[Test] + public function set_title_value_does_not_set_computed_fields() + { + Config::set('runway.resources.StatamicRadPack\Runway\Tests\Fixtures\Models\Post.title_field', 'appended_value'); + + Runway::discoverResources(); + + $resource = Runway::findResource('post'); + + $post = Post::factory()->create(); + $original = $post->getAttribute('appended_value'); + + $resource->setTitleValue($post, 'Should not be set'); + + $this->assertEquals($original, $post->getAttribute('appended_value')); + } + #[Test] public function revisions_can_be_enabled() { diff --git a/tests/__fixtures__/revisions/resources/post/1/1776426484.yaml b/tests/__fixtures__/revisions/resources/post/1/1776426484.yaml new file mode 100644 index 00000000..335a732b --- /dev/null +++ b/tests/__fixtures__/revisions/resources/post/1/1776426484.yaml @@ -0,0 +1,18 @@ +action: unpublish +date: 1776426484 +user: 15e7f3fa-4581-449c-b95c-1bbda174fad2 +message: 'Live live live!' +attributes: + id: 1 + published: false + data: + title: 'pariatur eum quae animi vero aliquam' + slug: pariatur-eum-quae-animi-vero-aliquam + body: 'Quae ut libero sed veritatis unde. Cumque velit sunt quia modi possimus ullam aut. Ipsa deleniti laudantium ratione deleniti dolorum debitis eos. Eum iure officiis aspernatur qui qui aperiam enim. Dolor et necessitatibus qui quis et iusto vel. Autem est aperiam eum et rem libero aliquid. Omnis consequatur dolor facilis voluptas velit. Voluptatum sit ratione suscipit aut. Voluptatem consequatur laudantium voluptatem qui dolore impedit consequatur explicabo. Provident at omnis doloremque repellat expedita possimus consequuntur. Enim perferendis corporis vel non blanditiis. Cumque voluptatum veritatis molestiae rerum sunt. Similique ut consequatur recusandae repudiandae dolorem laboriosam est veniam. Quidem eum hic expedita nesciunt illum incidunt ad alias. Quia ad a sed maiores. Vitae aliquid inventore saepe sint voluptatem. Cum qui corporis vero rerum provident est perspiciatis. Aut minus iusto id ducimus eos pariatur. Rerum rem distinctio quo pariatur vel perspiciatis. Soluta voluptas consequatur vero velit reiciendis sed fuga. Error voluptatem omnis quia. Aspernatur temporibus iste molestias reiciendis aut rem exercitationem. Neque quisquam vero corporis voluptatem vitae. Distinctio est aspernatur sed voluptas eaque. Vel tenetur et possimus impedit. Voluptates aspernatur officia ex enim. Perferendis facilis in et voluptatem nisi perspiciatis consequatur. Unde id praesentium distinctio amet illo deleniti. Totam voluptate impedit quia natus est. Et officia nam placeat illum. Amet ea voluptates ipsam eos ducimus.' + excerpt: 'This is an excerpt.' + author_id: 1 + start_date: '2026-04-17 11:48:04' + end_date: null + membership_status: null + values: null + external_links: null