diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 06a18e7c0e..2b58343161 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -673,7 +673,7 @@ protected function revisionAttributes() 'slug' => $this->slug(), 'published' => $this->published(), 'date' => $this->collection()->dated() ? $this->date()->timestamp : null, - 'data' => $this->data()->except(['updated_by', 'updated_at'])->all(), + 'data' => $this->data()->except(['updated_by', 'updated_at', ...$this->nonRevisableFields()])->all(), ]; } @@ -689,7 +689,7 @@ public function makeFromRevision($revision) $entry ->published($attrs['published']) - ->data($attrs['data']) + ->data($this->data()->merge($attrs['data'])) ->slug($attrs['slug']); if ($this->collection()->dated() && ($date = Arr::get($attrs, 'date'))) { diff --git a/src/Fields/Field.php b/src/Fields/Field.php index b7b9f4f17e..75b765169e 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -248,6 +248,11 @@ public function isFilterable() return (bool) $this->get('filterable'); } + public function isRevisable() + { + return (bool) $this->get('revisable', true); + } + public function shouldBeDuplicated() { if (is_null($this->get('duplicate'))) { @@ -269,6 +274,7 @@ public function toPublishArray() 'visibility' => $this->visibility(), 'read_only' => $this->visibility() === 'read_only', // Deprecated: Addon fieldtypes should now reference new `visibility` state. 'always_save' => $this->alwaysSave(), + 'revisable' => $this->isRevisable(), ]); } @@ -567,6 +573,13 @@ public static function commonFieldOptions(): Fields 'validate' => 'boolean', 'default' => true, ], + 'revisable' => [ + 'display' => __('Revisable'), + 'instructions' => __('statamic::messages.fields_revisable_instructions'), + 'type' => 'toggle', + 'validate' => 'boolean', + 'default' => true, + ], ])->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 5535e11a30..eff42358c2 100644 --- a/src/Http/Controllers/CP/Collections/EntriesController.php +++ b/src/Http/Controllers/CP/Collections/EntriesController.php @@ -6,6 +6,7 @@ use Illuminate\Validation\ValidationException; use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\CP\Breadcrumbs; +use Statamic\Entries\Entry as EntriesEntry; use Statamic\Exceptions\BlueprintNotFoundException; use Statamic\Facades\Action; use Statamic\Facades\Asset; @@ -13,6 +14,7 @@ use Statamic\Facades\Site; use Statamic\Facades\Stache; use Statamic\Facades\User; +use Statamic\Fields\Field; use Statamic\Hooks\CP\EntriesIndexQuery; use Statamic\Http\Controllers\CP\CpController; use Statamic\Http\Requests\FilteredRequest; @@ -261,6 +263,9 @@ public function update(Request $request, $collection, $entry) ->user(User::current()) ->save(); + // have to save in case there are non-revisable fields + $this->saveNonRevisableFields($entry); + // catch any changes through RevisionSaving event $entry = $entry->fromWorkingCopy(); } else { @@ -563,4 +568,16 @@ protected function ensureCollectionIsAvailableOnSite($collection, $site) return redirect()->back()->with('error', __('Collection is not available on site ":handle".', ['handle' => $site->handle])); } } + + private function saveNonRevisableFields(EntriesEntry $entry): void + { + /** @var EntriesEntry */ + $savedVersion = $entry->fresh(); + + $entry->blueprint()->fields()->all() + ->reject(fn (Field $field) => $field->isRevisable()) + ->each(fn ($ignore, string $fieldHandle) => $savedVersion->set($fieldHandle, $entry->{$fieldHandle})); + + $savedVersion->save(); + } } diff --git a/src/Revisions/Revisable.php b/src/Revisions/Revisable.php index 2c1dfd0efc..f64bed5ecc 100644 --- a/src/Revisions/Revisable.php +++ b/src/Revisions/Revisable.php @@ -177,6 +177,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 e50009df67..5b9b60ef39 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -45,7 +45,16 @@ public function it_gets_revisions() { $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(); @@ -58,6 +67,7 @@ public function it_gets_revisions() 'blueprint' => 'test', 'title' => 'Original title', 'foo' => 'bar', + 'bar' => 'foo', ])->create(); tap($entry->makeRevision(), function ($copy) { @@ -85,6 +95,7 @@ public function it_gets_revisions() ->assertJsonPath('0.revisions.0.message', 'Revision one') ->assertJsonPath('0.revisions.0.attributes.data.title', 'Original title') ->assertJsonPath('0.revisions.0.attributes.item_url', 'http://localhost/cp/collections/blog/entries/1/revisions/'.Carbon::parse('2017-02-01')->timestamp) + ->assertJsonPath('0.revisions.0.attributes.data.bar', null) ->assertJsonPath('1.revisions.0.action', 'revision') ->assertJsonPath('1.revisions.0.message', false) @@ -102,7 +113,16 @@ 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(); @@ -115,6 +135,7 @@ public function it_publishes_an_entry() 'blueprint' => 'test', 'title' => 'Title', 'foo' => 'bar', + 'bar' => 'foo', ])->create(); tap($entry->makeWorkingCopy(), function ($copy) { @@ -137,6 +158,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()); @@ -166,7 +188,16 @@ public function it_unpublishes_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(); @@ -179,6 +210,7 @@ public function it_unpublishes_an_entry() 'blueprint' => 'test', 'title' => 'Title', 'foo' => 'bar', + 'bar' => 'foo', ])->create(); $this->assertTrue($entry->published()); @@ -194,6 +226,7 @@ public function it_unpublishes_an_entry() 'blueprint' => 'test', 'title' => 'Title', 'foo' => 'bar', + 'bar' => 'foo', 'updated_at' => $now->timestamp, 'updated_by' => $user->id(), ], $entry->data()->all()); @@ -219,7 +252,16 @@ public function it_unpublishes_an_entry() #[Test] public function it_creates_a_revision() { - $this->setTestBlueprint('test', ['foo' => ['type' => 'text']]); + $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(); @@ -232,6 +274,7 @@ public function it_creates_a_revision() 'blueprint' => 'test', 'title' => 'Title', 'foo' => 'bar', + 'bar' => 'foo', ])->create(); tap($entry->makeWorkingCopy(), function ($copy) { @@ -253,6 +296,7 @@ public function it_creates_a_revision() 'blueprint' => 'test', 'title' => 'Title', 'foo' => 'bar', + 'bar' => 'foo', ], $entry->data()->all()); $this->assertFalse($entry->published()); $this->assertCount(1, $entry->revisions()); diff --git a/tests/Feature/Entries/UpdateEntryTest.php b/tests/Feature/Entries/UpdateEntryTest.php index 4531ade84d..5962858368 100644 --- a/tests/Feature/Entries/UpdateEntryTest.php +++ b/tests/Feature/Entries/UpdateEntryTest.php @@ -11,6 +11,7 @@ use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; +use Statamic\Facades\Folder; use Statamic\Facades\Role; use Statamic\Facades\User; use Statamic\Structures\CollectionStructure; @@ -23,6 +24,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() { @@ -416,7 +436,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] @@ -501,7 +555,7 @@ public function does_not_validate_max_depth_when_collection_max_depth_is_null() ->assertOk(); } - private function seedUserAndCollection() + private function seedUserAndCollection(bool $enableRevisions = false) { $this->setTestRoles(['test' => [ 'access cp', @@ -510,7 +564,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 a13c952be8..60e59d5139 100644 --- a/tests/Fields/BlueprintTest.php +++ b/tests/Fields/BlueprintTest.php @@ -436,6 +436,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'validate' => 'required|min:2', 'input_type' => 'text', @@ -474,6 +475,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'textarea', 'placeholder' => null, 'validate' => 'min:2', @@ -563,6 +565,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'placeholder' => null, @@ -589,6 +592,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'placeholder' => null, diff --git a/tests/Fields/FieldTest.php b/tests/Fields/FieldTest.php index 62c9221748..d3c0f5c05a 100644 --- a/tests/Fields/FieldTest.php +++ b/tests/Fields/FieldTest.php @@ -342,6 +342,7 @@ public function preProcess($data) 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'example', 'validate' => 'required', 'foo' => 'bar', @@ -647,4 +648,20 @@ 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_gets_revisable() + { + $field = new Field('test', ['type' => 'text', 'revisable' => false]); + + $this->assertFalse($field->isRevisable()); + } } diff --git a/tests/Fields/FieldsTest.php b/tests/Fields/FieldsTest.php index 057bc08d2a..fddcdcd290 100644 --- a/tests/Fields/FieldsTest.php +++ b/tests/Fields/FieldsTest.php @@ -434,6 +434,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, ], [ 'handle' => 'two', @@ -457,6 +458,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, ], ], $fields->toPublishArray()); } @@ -522,6 +524,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, ], [ 'handle' => 'nested_deeper_two', @@ -548,6 +551,7 @@ public function converts_to_array_suitable_for_rendering_prefixed_conditional_fi 'sortable' => true, 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, ], ], $fields->toPublishArray()); } diff --git a/tests/Fields/SectionTest.php b/tests/Fields/SectionTest.php index 0e09f08b90..0afb84b986 100644 --- a/tests/Fields/SectionTest.php +++ b/tests/Fields/SectionTest.php @@ -125,6 +125,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'validate' => 'required|min:2', 'input_type' => 'text', @@ -152,6 +153,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'textarea', 'validate' => 'min:2', 'placeholder' => null, diff --git a/tests/Fields/TabTest.php b/tests/Fields/TabTest.php index ad0d34d3bc..f6f1b0c98c 100644 --- a/tests/Fields/TabTest.php +++ b/tests/Fields/TabTest.php @@ -150,6 +150,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'validate' => 'required|min:2', 'input_type' => 'text', @@ -177,6 +178,7 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'textarea', 'validate' => 'min:2', 'placeholder' => null, diff --git a/tests/Fieldtypes/NestedFieldsTest.php b/tests/Fieldtypes/NestedFieldsTest.php index d35817067a..5f20838d86 100644 --- a/tests/Fieldtypes/NestedFieldsTest.php +++ b/tests/Fieldtypes/NestedFieldsTest.php @@ -23,6 +23,7 @@ public function it_preprocesses_each_value_when_used_for_config() ->andReturn(new class extends Fieldtype { protected $component = 'assets'; + protected $configFields = [ 'max_files' => ['type' => 'integer'], 'container' => ['type' => 'plain'], @@ -84,6 +85,7 @@ public function preProcess($data) 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'assets', 'max_files' => 2, 'container' => 'main', diff --git a/tests/Fieldtypes/SetsTest.php b/tests/Fieldtypes/SetsTest.php index e57b713568..f0e29b94cd 100644 --- a/tests/Fieldtypes/SetsTest.php +++ b/tests/Fieldtypes/SetsTest.php @@ -227,6 +227,7 @@ public function it_preprocesses_for_config_with_groups() 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'placeholder' => null, @@ -264,6 +265,7 @@ public function it_preprocesses_for_config_with_groups() 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'placeholder' => null, @@ -324,6 +326,7 @@ public function it_preprocesses_for_config_without_groups() 'visibility' => 'visible', 'replicator_preview' => true, 'duplicate' => true, + 'revisable' => true, 'type' => 'text', 'input_type' => 'text', 'placeholder' => null, diff --git a/tests/Revisions/RevisableTest.php b/tests/Revisions/RevisableTest.php new file mode 100644 index 0000000000..3fe74c9f58 --- /dev/null +++ b/tests/Revisions/RevisableTest.php @@ -0,0 +1,85 @@ + __DIR__.'/__fixtures__']); + $this->repo = (new RevisionRepository); + } + + #[Test] + public function it_gets_non_revisable_fields() + { + $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'); + + $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"); + + $revision = $this->repo->whereKey('123')->first(); + + $this->assertEquals( + collect([ + 'revisable' => 'overridden', + 'non_revisable' => "don't override me", + ]), + $entry->makeFromRevision($revision)->data() + ); + } +} diff --git a/tests/Revisions/__fixtures__/123/1553546421.yaml b/tests/Revisions/__fixtures__/123/1553546421.yaml index 833a259984..cb828ee7eb 100644 --- a/tests/Revisions/__fixtures__/123/1553546421.yaml +++ b/tests/Revisions/__fixtures__/123/1553546421.yaml @@ -1,2 +1,8 @@ date: 1553546421 -attributes: {} +attributes: + id: 123 + slug: the-slug + published: true + date: 1633590000 + data: + revisable: 'overridden'