diff --git a/lang/en/messages.php b/lang/en/messages.php index 0e97bc7c730..705ae63ed70 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -107,6 +107,7 @@ 'fields_instructions_position_instructions' => 'Show instructions above or below the field.', 'fields_listable_instructions' => 'Control the listing column visibility.', 'fields_replicator_preview_instructions' => 'Control preview visibility in Replicator/Bard sets.', + 'fields_revisable_instructions' => 'Include this field in revisions. When disabled, changes are saved straight to the published entry instead of a working copy. Only applies to top level fields in collection entries. The date, slug and published fields are unaffected.', 'fields_sortable_instructions' => 'Control if the field should be sortable in listing views.', 'fields_visibility_instructions' => 'Control field visibility on publish forms.', 'fieldset_import_fieldset_instructions' => 'The fieldset to be imported.', diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index b1113005624..9952606906c 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -762,7 +762,7 @@ protected function revisionAttributes() 'slug' => $this->slug(), 'published' => $this->published(), 'date' => $this->collection()->dated() && (! $this->hasOrigin() || $this->hasExplicitDate()) ? $this->date()->timestamp : null, - 'data' => $this->data()->except(['updated_by', 'updated_at'])->all(), + 'data' => $this->data()->except(['updated_by', 'updated_at', ...$this->nonRevisableFields()])->all(), ]; } @@ -778,7 +778,7 @@ public function makeFromRevision($revision) $entry ->published($attrs['published']) - ->data($attrs['data']) + ->data(collect($attrs['data'])->merge($this->data()->only($this->nonRevisableFields()))) ->slug($attrs['slug']); if ($this->collection()->dated()) { diff --git a/src/Fields/Field.php b/src/Fields/Field.php index ea36ea2236e..b021eb3072e 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -264,6 +264,19 @@ public function isFilterable() return (bool) $this->get('filterable'); } + public function isRevisable() + { + if ($this->visibility() === 'computed') { + return true; + } + + if (is_null($this->get('revisable'))) { + return true; + } + + return (bool) $this->get('revisable'); + } + public function shouldBeDuplicated() { if (is_null($this->get('duplicate'))) { @@ -623,6 +636,17 @@ public static function commonFieldOptions(): Fields 'width' => 50, ], + 'revisable' => [ + 'display' => __('Revisable'), + 'instructions' => __('statamic::messages.fields_revisable_instructions'), + 'type' => 'toggle', + 'validate' => 'boolean', + 'default' => true, + 'unless' => [ + 'visibility' => 'equals computed', + ], + 'width' => 50, + ], ])->map(fn ($field, $handle) => compact('handle', 'field'))->values()->all(); return new ConfigFields($fields); diff --git a/src/Http/Controllers/CP/Collections/EntriesController.php b/src/Http/Controllers/CP/Collections/EntriesController.php index d1a7a7fd4b7..aa14c715b7e 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -258,6 +258,11 @@ public function update(Request $request, $collection, $entry) ->user(User::current()) ->save(); + // have to save in case there are non-revisable fields + if ($saved) { + $this->saveNonRevisableFields($entry); + } + // catch any changes through RevisionSaving event $entry = $entry->fromWorkingCopy(); } else { @@ -531,4 +536,23 @@ protected function ensureCollectionIsAvailableOnSite($collection, $site) return redirect()->back()->with('error', __('Collection is not available on site ":handle".', ['handle' => $site->handle])); } } + + private function saveNonRevisableFields(EntryContract $entry): void + { + $fresh = $entry->fresh(); + $handles = collect($entry->nonRevisableFields()); + $values = $entry->data()->only($handles->all()); + + // A localization's data only holds the localized handles, so a non-revisable + // field missing from it has been synced back to the origin and must be removed. + $removed = $handles->diff($values->keys())->filter(fn ($handle) => $fresh->has($handle)); + + if ($values->isEmpty() && $removed->isEmpty()) { + return; + } + + $removed->each(fn ($handle) => $fresh->remove($handle)); + + $fresh->merge($values)->updateLastModified(User::current())->save(); + } } diff --git a/src/Revisions/Revisable.php b/src/Revisions/Revisable.php index 8091eaa1d73..63870eb85aa 100644 --- a/src/Revisions/Revisable.php +++ b/src/Revisions/Revisable.php @@ -172,6 +172,16 @@ public function revisionsEnabled() return config('statamic.revisions.enabled') && Statamic::pro(); } + public function nonRevisableFields(): array + { + return $this->blueprint() + ->fields() + ->all() + ->reject(fn ($field) => $field->isRevisable()) + ->keys() + ->all(); + } + abstract protected function revisionKey(); abstract protected function revisionAttributes(); diff --git a/tests/Feature/Entries/EntryRevisionsTest.php b/tests/Feature/Entries/EntryRevisionsTest.php index 8953729b239..ecc6ac0f68f 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -203,7 +203,10 @@ public function it_publishes_an_entry() { $now = Carbon::parse('2017-02-03'); Carbon::setTestNow($now); - $this->setTestBlueprint('test', ['foo' => ['type' => 'text']]); + $this->setTestBlueprint('test', [ + 'foo' => ['type' => 'text'], + 'bar' => ['type' => 'text', 'revisable' => false], + ]); $this->setTestRoles(['test' => ['access cp', 'publish blog entries']]); $user = User::make()->id('user-1')->assignRole('test')->save(); @@ -216,6 +219,7 @@ public function it_publishes_an_entry() 'blueprint' => 'test', 'title' => 'Title', 'foo' => 'bar', + 'bar' => 'foo', ])->create(); tap($entry->makeWorkingCopy(), function ($copy) { @@ -238,6 +242,7 @@ public function it_publishes_an_entry() 'blueprint' => 'test', 'title' => 'Title', 'foo' => 'foo modified in working copy', + 'bar' => 'foo', 'updated_at' => $now->timestamp, 'updated_by' => $user->id(), ], $entry->data()->all()); @@ -535,6 +540,48 @@ public function it_restores_an_unpublished_entrys_contents_to_another_revision() $this->assertCount(1, $entry->revisions()); } + #[Test] + public function it_keeps_non_revisable_fields_when_restoring_an_unpublished_entrys_contents() + { + $this->setTestBlueprint('test', [ + 'foo' => ['type' => 'text'], + 'bar' => ['type' => 'text', 'revisable' => false], + ]); + $this->setTestRoles(['test' => ['access cp', 'edit blog entries']]); + $user = User::make()->id('user-1')->assignRole('test')->save(); + + tap((new Revision) + ->key('collections/blog/en/123') + ->date(Carbon::createFromTimestamp('1553546421', config('app.timezone'))) + ->attributes([ + 'published' => true, + 'slug' => 'existing-slug', + 'data' => ['foo' => 'existing foo'], + ]))->save(); + + $entry = EntryFactory::id('123') + ->slug('test') + ->collection('blog') + ->published(false) + ->data([ + 'blueprint' => 'test', + 'foo' => 'bar', + 'bar' => 'not tracked by revisions', + 'stale' => 'not in the revision', + ])->create(); + + $this + ->actingAs($user) + ->restore($entry, ['revision' => '1553546421']) + ->assertOk() + ->assertSessionHas('success'); + + $entry = Entry::find($entry->id()); + $this->assertEquals('existing foo', $entry->get('foo')); + $this->assertEquals('not tracked by revisions', $entry->get('bar')); + $this->assertFalse($entry->has('stale')); + } + #[Test] public function localized_entry_with_inherited_localizable_date_keeps_following_its_origin_when_reconstructed_from_revision() { diff --git a/tests/Feature/Entries/UpdateEntryTest.php b/tests/Feature/Entries/UpdateEntryTest.php index 694b8795bb3..9af33c48a02 100644 --- a/tests/Feature/Entries/UpdateEntryTest.php +++ b/tests/Feature/Entries/UpdateEntryTest.php @@ -8,9 +8,11 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Events\EntrySaving; +use Statamic\Events\RevisionSaving; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; +use Statamic\Facades\Folder; use Statamic\Facades\Role; use Statamic\Facades\Taxonomy; use Statamic\Facades\Term; @@ -25,6 +27,25 @@ class UpdateEntryTest extends TestCase use FakesRoles; use PreventSavingStacheItemsToDisk; + public function setUp(): void + { + parent::setUp(); + + $this->dir = __DIR__.'/tmp'; + + config([ + 'statamic.editions.pro' => true, + 'statamic.revisions.path' => $this->dir, + 'statamic.revisions.enabled' => true, + ]); + } + + public function tearDown(): void + { + Folder::delete($this->dir); + parent::tearDown(); + } + #[Test] public function it_denies_access_if_you_dont_have_edit_permission() { @@ -535,6 +556,188 @@ public function auto_title_only_gets_saved_on_localization_when_different_from_o $this->assertNull($localization->get('title')); } + #[Test] + public function non_revisable_fields_are_saved_with_raw_values() + { + [$user, $collection] = $this->seedUserAndCollection(true); + + $this->seedBlueprintFields($collection, [ + 'related' => ['type' => 'entries', 'revisable' => false], + 'when' => ['type' => 'date', 'format' => 'Y-m-d', 'revisable' => false], + ]); + + $entry = EntryFactory::id('1') + ->slug('test') + ->collection('test') + ->published(true) + ->data(['title' => 'Test']) + ->create(); + + $this + ->actingAs($user) + ->update($entry, [ + 'related' => ['2', '3'], + 'when' => '2024-03-04', + ]) + ->assertOk(); + + $entry = Entry::find($entry->id()); + $this->assertSame(['2', '3'], $entry->get('related')); + $this->assertSame('2024-03-04', $entry->get('when')); + } + + #[Test] + public function non_revisable_fields_are_not_saved_when_the_revision_is_cancelled() + { + [$user, $collection] = $this->seedUserAndCollection(true); + + $this->seedBlueprintFields($collection, [ + 'foo' => ['type' => 'text', 'revisable' => false], + ]); + + $entry = EntryFactory::id('1') + ->slug('test') + ->collection('test') + ->published(true) + ->data(['title' => 'Test', 'foo' => 'bar']) + ->create(); + + Event::listen(RevisionSaving::class, fn () => false); + + $this + ->actingAs($user) + ->update($entry, ['foo' => 'not bar']) + ->assertOk(); + + $this->assertEquals('bar', $entry->fresh()->get('foo')); + } + + #[Test] + public function non_revisable_fields_are_not_removed_from_an_entry_without_an_origin() + { + [$user, $collection] = $this->seedUserAndCollection(true); + + $this->seedBlueprintFields($collection, [ + 'foo' => ['type' => 'text', 'revisable' => false], + ]); + + $entry = EntryFactory::id('1') + ->slug('test') + ->collection('test') + ->published(true) + ->data(['title' => 'Test', 'foo' => 'bar']) + ->create(); + + // A single site install doesn't send _localized at all. + $this + ->actingAs($user) + ->update($entry, ['foo' => 'bar']) + ->assertOk(); + + $this->assertEquals('bar', $entry->fresh()->get('foo')); + } + + #[Test] + public function published_entry_without_non_revisable_fields_is_not_saved() + { + [$user, $collection] = $this->seedUserAndCollection(true); + + $this->seedBlueprintFields($collection, [ + 'foo' => ['type' => 'text'], + ]); + + $entry = EntryFactory::id('1') + ->slug('test') + ->collection('test') + ->published(true) + ->data(['title' => 'Test']) + ->create(); + + Event::fake([EntrySaving::class]); + + $this + ->actingAs($user) + ->update($entry, ['foo' => 'bar']) + ->assertOk(); + + Event::assertNotDispatched(EntrySaving::class); + $this->assertTrue($entry->fresh()->hasWorkingCopy()); + } + + #[Test] + public function non_revisable_fields_are_only_saved_to_the_localization_while_localized() + { + $this->setSites([ + 'en' => ['locale' => 'en', 'url' => '/'], + 'fr' => ['locale' => 'fr', 'url' => '/fr/'], + ]); + + [$user, $collection] = $this->seedUserAndCollection(true); + $collection->sites(['en', 'fr'])->save(); + + $this->seedBlueprintFields($collection, [ + 'foo' => ['type' => 'text', 'revisable' => false], + ]); + + $origin = EntryFactory::collection($collection) + ->locale('en') + ->slug('origin') + ->published(true) + ->data(['title' => 'Origin', 'foo' => 'bar']) + ->create(); + + $localization = EntryFactory::collection($collection) + ->locale('fr') + ->origin($origin) + ->slug('localization') + ->published(true) + ->create(); + + $this + ->actingAs($user) + ->update($localization, [ + 'foo' => 'bar', + '_localized' => [], + ]) + ->assertOk(); + + $localization = $localization->fresh(); + $this->assertFalse($localization->has('foo')); + $this->assertEquals('bar', $localization->foo); + + $this + ->actingAs($user) + ->update($localization, [ + 'foo' => 'le bar', + '_localized' => ['foo'], + ]) + ->assertOk(); + + $localization = $localization->fresh(); + $this->assertEquals('le bar', $localization->get('foo')); + $this->assertEquals('bar', $origin->fresh()->get('foo')); + + $this + ->actingAs($user) + ->update($localization, [ + 'foo' => 'bar', + '_localized' => [], + ]) + ->assertOk(); + + $localization = $localization->fresh(); + $this->assertFalse($localization->has('foo')); + $this->assertEquals('bar', $localization->foo); + + // Publishing merges non-revisable fields back from the live entry, which no + // longer has the handle, so the field stays synced with the origin. + $localization->publishWorkingCopy(['user' => $user]); + + $localization = $localization->fresh(); + $this->assertFalse($localization->has('foo')); + $this->assertEquals('bar', $localization->foo); + } + #[Test] public function it_can_validate_against_published_value() { @@ -558,7 +761,41 @@ public function it_can_validate_against_published_value() #[Test] public function published_entry_gets_saved_to_working_copy() { - $this->markTestIncomplete(); + [$user, $collection] = $this->seedUserAndCollection(true); + + $this->seedBlueprintFields($collection, [ + 'revisable' => ['type' => 'text'], + 'non_revisable' => ['type' => 'text', 'revisable' => false], + ]); + + $entry = EntryFactory::id('1') + ->slug('test') + ->collection('test') + ->data(['title' => 'Revisable Test', 'published' => true]) + ->create(); + + $this + ->actingAs($user) + ->update($entry, [ + 'revisable' => 'revise me', + 'non_revisable' => 'no revisions for you', + ]) + ->assertOk(); + + $entry = Entry::find($entry->id()); + $this->assertEquals('no revisions for you', $entry->non_revisable); + $this->assertEquals('Revisable Test', $entry->title); + $this->assertEquals('test', $entry->slug()); + $this->assertNull($entry->revisable); + + $workingCopy = $entry->fromWorkingCopy(); + $this->assertEquals('updated-entry', $workingCopy->slug()); + $this->assertEquals([ + 'title' => 'Updated entry', + 'revisable' => 'revise me', + 'non_revisable' => 'no revisions for you', + 'published' => true, + ], $workingCopy->data()->all()); } #[Test] @@ -646,7 +883,7 @@ public function it_prevents_duplicate_uris_for_structured_entries_with_depth_con ->assertJsonValidationErrors(['slug']); } - private function seedUserAndCollection() + private function seedUserAndCollection(bool $enableRevisions = false) { $this->setTestRoles(['test' => [ 'access cp', @@ -655,7 +892,7 @@ private function seedUserAndCollection() 'access fr site', ]]); $user = tap(User::make()->assignRole('test'))->save(); - $collection = tap(Collection::make('test'))->save(); + $collection = tap(Collection::make('test')->revisionsEnabled($enableRevisions))->save(); return [$user, $collection]; } diff --git a/tests/Fields/BlueprintTest.php b/tests/Fields/BlueprintTest.php index 5a947455ed8..2af48e344aa 100644 --- a/tests/Fields/BlueprintTest.php +++ b/tests/Fields/BlueprintTest.php @@ -451,6 +451,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, @@ -490,6 +491,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'textarea', 'placeholder' => null, 'character_limit' => null, @@ -594,6 +596,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, @@ -622,6 +625,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, diff --git a/tests/Fields/FieldTest.php b/tests/Fields/FieldTest.php index 38f3361593f..1817a428f5b 100644 --- a/tests/Fields/FieldTest.php +++ b/tests/Fields/FieldTest.php @@ -345,6 +345,7 @@ public function preProcess($data) 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'example', 'foo' => 'bar', 'a_config_field_with_pre_processing' => 'foo preprocessed', @@ -722,4 +723,36 @@ public function it_gets_and_sets_the_form() $this->assertEquals($field, $return); $this->assertEquals($form, $field->form()); } + + #[Test] + public function it_defaults_to_revisable() + { + $field = new Field('test', ['type' => 'text']); + + $this->assertTrue($field->isRevisable()); + } + + #[Test] + public function it_defaults_to_revisable_when_null() + { + $field = new Field('test', ['type' => 'text', 'revisable' => null]); + + $this->assertTrue($field->isRevisable()); + } + + #[Test] + public function it_gets_revisable() + { + $field = new Field('test', ['type' => 'text', 'revisable' => false]); + + $this->assertFalse($field->isRevisable()); + } + + #[Test] + public function its_revisable_when_computed() + { + $field = new Field('test', ['type' => 'text', 'revisable' => false, 'visibility' => 'computed']); + + $this->assertTrue($field->isRevisable()); + } } diff --git a/tests/Fields/FieldsTest.php b/tests/Fields/FieldsTest.php index 711b27acb3e..0d34c6848a6 100644 --- a/tests/Fields/FieldsTest.php +++ b/tests/Fields/FieldsTest.php @@ -491,6 +491,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'actions' => true, ], [ @@ -515,6 +516,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'actions' => true, 'rows' => null, ], @@ -583,6 +585,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'actions' => true, ], [ @@ -611,6 +614,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'actions' => true, ], ], $fields->toPublishArray()); diff --git a/tests/Fields/SectionTest.php b/tests/Fields/SectionTest.php index 88d4aa94d51..332fc8a267c 100644 --- a/tests/Fields/SectionTest.php +++ b/tests/Fields/SectionTest.php @@ -126,6 +126,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, @@ -154,6 +155,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'textarea', 'placeholder' => null, 'character_limit' => null, diff --git a/tests/Fields/TabTest.php b/tests/Fields/TabTest.php index 1af68e3ae09..778b46a2969 100644 --- a/tests/Fields/TabTest.php +++ b/tests/Fields/TabTest.php @@ -153,6 +153,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, @@ -181,6 +182,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'textarea', 'placeholder' => null, 'character_limit' => null, diff --git a/tests/Fieldtypes/NestedFieldsTest.php b/tests/Fieldtypes/NestedFieldsTest.php index 67ac5aca271..5a024dd4a10 100644 --- a/tests/Fieldtypes/NestedFieldsTest.php +++ b/tests/Fieldtypes/NestedFieldsTest.php @@ -85,6 +85,7 @@ public function preProcess($data) 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'assets', 'max_files' => 2, 'container' => 'main', diff --git a/tests/Fieldtypes/SetsTest.php b/tests/Fieldtypes/SetsTest.php index e90c46b00fb..fa585b8a00c 100644 --- a/tests/Fieldtypes/SetsTest.php +++ b/tests/Fieldtypes/SetsTest.php @@ -233,6 +233,7 @@ public function it_preprocesses_for_config_with_groups() 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, @@ -273,6 +274,7 @@ public function it_preprocesses_for_config_with_groups() 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, @@ -336,6 +338,7 @@ public function it_preprocesses_for_config_without_groups() 'replicator_preview' => true, 'duplicate' => true, 'actions' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'character_limit' => null, diff --git a/tests/Revisions/RevisableTest.php b/tests/Revisions/RevisableTest.php new file mode 100644 index 00000000000..0a92ca65e3b --- /dev/null +++ b/tests/Revisions/RevisableTest.php @@ -0,0 +1,83 @@ + ['type' => 'text'], + 'non_revisable' => ['type' => 'text', 'revisable' => false], + ]); + BlueprintRepository::shouldReceive('in')->with('collections/blog')->andReturn(collect(['blog' => $blueprint])); + Collection::make('blog')->save(); + + $entry = (new Entry)->collection('blog')->id('123'); + + $this->assertEquals(['non_revisable'], $entry->nonRevisableFields()); + } + + #[Test] + public function it_sets_proper_revision_attributes() + { + $blueprint = Blueprint::makeFromFields([ + 'revisable' => ['type' => 'text'], + 'non_revisable' => ['type' => 'text', 'revisable' => false], + ]); + BlueprintRepository::shouldReceive('in')->with('collections/blog')->andReturn(collect(['blog' => $blueprint])); + Collection::make('blog')->save(); + + $entry = (new Entry) + ->collection('blog') + ->id('123') + ->set('revisable', 'see me') + ->set('non_revisable', "don't see me"); + + $this->assertEquals(['revisable' => 'see me'], $entry->makeRevision()->attributes()['data']); + } + + #[Test] + public function it_can_make_entry_from_revision() + { + $blueprint = Blueprint::makeFromFields([ + 'revisable' => ['type' => 'text'], + 'non_revisable' => ['type' => 'text', 'revisable' => false], + ]); + + BlueprintRepository::shouldReceive('in')->with('collections/blog')->andReturn(collect(['blog' => $blueprint])); + Collection::make('blog')->save(); + + $entry = (new Entry)->collection('blog')->id('123'); + $entry + ->set('revisable', 'override me') + ->set('non_revisable', "don't override me") + ->set('stale', 'not in the revision, so drop me'); + + $revision = (new Revision)->attributes([ + 'published' => true, + 'slug' => 'the-slug', + 'data' => ['revisable' => 'overridden'], + ]); + + $this->assertEquals( + collect([ + 'revisable' => 'overridden', + 'non_revisable' => "don't override me", + ]), + $entry->makeFromRevision($revision)->data() + ); + } +}