From b2817895ed3bad59fcfd53683979377bc20fa6ab Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 26 Feb 2025 14:31:18 -0800 Subject: [PATCH 01/42] wip --- .../js/components/entries/PublishActions.vue | 135 ++++++++++-------- 1 file changed, 79 insertions(+), 56 deletions(-) diff --git a/resources/js/components/entries/PublishActions.vue b/resources/js/components/entries/PublishActions.vue index 6fb7919f6cb..4ee9c83ffa2 100644 --- a/resources/js/components/entries/PublishActions.vue +++ b/resources/js/components/entries/PublishActions.vue @@ -2,70 +2,80 @@
+ +
+
+ {{ __('Publish') }} +
-
- {{ __('Publish') }} -
- -
- -
- -
- - - -
+
+
@@ -86,6 +96,18 @@ export default { data() { return { action: this.canManagePublishState ? 'publish' : 'revision', + config: { + earliest_date: { date: null, time: null}, + latest_date: { date: null, time: null}, + mode: 'single', + type: 'single', + inline: false, + time_enabled: true, + default: 'now', + rows: 1, + columns: 1 + }, + publishRevisionAt: { date: '2025-02-28', time: null}, revisionMessage: null, saving: false, } @@ -98,6 +120,7 @@ export default { if (this.canManagePublishState) { options.push({ value: 'publish', label: __('Publish Now') }); + options.push({ value: 'publish_later', label: __('Publish Later') }); if (this.published) { options.push({ value: 'unpublish', label: __('Unpublish') }); From c0488d839f13da4d5f2793b16734c4417ae44c16 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 26 Feb 2025 14:54:46 -0800 Subject: [PATCH 02/42] tidy --- .../js/components/entries/PublishActions.vue | 129 ++++++++---------- 1 file changed, 60 insertions(+), 69 deletions(-) diff --git a/resources/js/components/entries/PublishActions.vue b/resources/js/components/entries/PublishActions.vue index 4ee9c83ffa2..12542466e6c 100644 --- a/resources/js/components/entries/PublishActions.vue +++ b/resources/js/components/entries/PublishActions.vue @@ -4,76 +4,70 @@
-
- {{ __('Publish') }} -
- -
- -
- +
+ {{ __('Publish') }} +
- -
+
@@ -97,17 +91,11 @@ export default { return { action: this.canManagePublishState ? 'publish' : 'revision', config: { - earliest_date: { date: null, time: null}, + earliest_date: this.now(), latest_date: { date: null, time: null}, - mode: 'single', - type: 'single', - inline: false, - time_enabled: true, - default: 'now', - rows: 1, - columns: 1 + time_enabled: true }, - publishRevisionAt: { date: '2025-02-28', time: null}, + publishRevisionAt: this.now(), revisionMessage: null, saving: false, } @@ -153,6 +141,13 @@ export default { methods: { + now() { + return { + date: moment().format('YYYY-MM-DD'), + time: moment().format('HH:mm') + }; + }, + submit() { this.saving = true; this.$emit('saving'); @@ -207,10 +202,6 @@ export default { }).catch(e => {}); }, - submitSchedule() { - // todo - }, - submitUnpublish() { const payload = { message: this.revisionMessage }; From f8377233a68976725ae98dc017b1debd44a4fb89 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 26 Feb 2025 15:03:30 -0800 Subject: [PATCH 03/42] Add info text --- resources/js/components/entries/PublishActions.vue | 2 ++ resources/lang/en/messages.php | 1 + 2 files changed, 3 insertions(+) diff --git a/resources/js/components/entries/PublishActions.vue b/resources/js/components/entries/PublishActions.vue index 12542466e6c..a06b19f0307 100644 --- a/resources/js/components/entries/PublishActions.vue +++ b/resources/js/components/entries/PublishActions.vue @@ -130,6 +130,8 @@ export default { return __('messages.publish_actions_unpublish'); case 'revision': return __('messages.publish_actions_create_revision'); + case 'publish_later': + return __('messages.publish_actions_schedule_revision'); } }, diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php index 69d3d54fe5e..f89565b22e9 100644 --- a/resources/lang/en/messages.php +++ b/resources/lang/en/messages.php @@ -191,6 +191,7 @@ 'publish_actions_current_becomes_draft_because_scheduled' => 'Since the current revision is published and you\'ve selected a date in the future, once you submit, the revision will act like a draft until the selected date.', 'publish_actions_publish' => 'Changes to the working copy will applied to the entry and it will be published immediately.', 'publish_actions_schedule' => 'Changes to the working copy will applied to the entry and it will be appear published on the selected date.', + 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', 'publish_actions_unpublish' => 'The current revision will be unpublished.', 'reset_password_notification_body' => 'You are receiving this email because we received a password reset request for your account.', 'reset_password_notification_no_action' => 'If you did not request a password reset, no further action is required.', From f50f330cba9a2cb5e040ac800fb3df6823ea3a6c Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 26 Feb 2025 16:24:50 -0800 Subject: [PATCH 04/42] save `publish_at` in the revision --- .../js/components/entries/PublishActions.vue | 16 ++++++++++++---- .../CP/Collections/EntryRevisionsController.php | 1 + src/Revisions/Revisable.php | 1 + src/Revisions/Revision.php | 15 +++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/resources/js/components/entries/PublishActions.vue b/resources/js/components/entries/PublishActions.vue index a06b19f0307..bf8a8f4cf37 100644 --- a/resources/js/components/entries/PublishActions.vue +++ b/resources/js/components/entries/PublishActions.vue @@ -30,7 +30,7 @@
this.handleAxiosError(e)); }, - submitRevision() { + submitPublishLater() { + this.submitRevision(this.publishRevisionAt); + }, + + submitRevision(publishRevisionAt) { const payload = { message: this.revisionMessage }; + if (publishRevisionAt) { + payload.publish_at = publishRevisionAt; + } + this.$axios.post(this.actions.createRevision, payload).then(response => { this.$toast.success(__('Revision created')); this.revisionMessage = null; diff --git a/src/Http/Controllers/CP/Collections/EntryRevisionsController.php b/src/Http/Controllers/CP/Collections/EntryRevisionsController.php index c1ef85e20f1..7f69004056e 100644 --- a/src/Http/Controllers/CP/Collections/EntryRevisionsController.php +++ b/src/Http/Controllers/CP/Collections/EntryRevisionsController.php @@ -41,6 +41,7 @@ public function store(Request $request, $collection, $entry) { $entry->createRevision([ 'message' => $request->message, + 'publish_at' => $request->publish_at, 'user' => User::fromUser($request->user()), ]); diff --git a/src/Revisions/Revisable.php b/src/Revisions/Revisable.php index 2c1dfd0efc7..8cc125ac760 100644 --- a/src/Revisions/Revisable.php +++ b/src/Revisions/Revisable.php @@ -169,6 +169,7 @@ public function createRevision($options = []) ->makeRevision() ->user($options['user'] ?? false) ->message($options['message'] ?? false) + ->publishAt($options['publish_at'] ?? null) ->save(); } diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index 171c1ca52e0..8e3955fd387 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -3,6 +3,7 @@ namespace Statamic\Revisions; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Support\Carbon; use Statamic\Contracts\Auth\User; use Statamic\Contracts\Revisions\Revision as Contract; use Statamic\Data\ExistsAsFile; @@ -23,6 +24,7 @@ class Revision implements Arrayable, Contract protected $user; protected $userId; protected $message; + protected $publishAt; protected $action = 'revision'; protected $attributes = []; @@ -61,6 +63,17 @@ public function message($message = null) return $this->fluentlyGetOrSet('message')->value($message); } + public function publishAt($dateTime = null) + { + if (is_null($dateTime)) { + return $this; + } + + $carbon = new Carbon($dateTime['date'].' '.$dateTime['time'] ?? '00:00'); + + return $this->fluentlyGetOrSet('publishAt')->value($carbon->timestamp); + } + public function attributes($attributes = null) { return $this->fluentlyGetOrSet('attributes')->value($attributes); @@ -103,6 +116,7 @@ public function fileData() 'date' => $this->date->timestamp, 'user' => $this->userId ?: null, 'message' => $this->message ?: null, + 'publish_at' => $this->publishAt ?: null, 'attributes' => $this->attributes, ]; } @@ -125,6 +139,7 @@ public function toArray() 'date' => $this->date()->timestamp, 'user' => $user, 'message' => $this->message, + 'publish_at' => $this->publishAt, 'attributes' => $this->attributes, ]; } From bc3b5dafd395cb1e70655f26437d103237bf2a85 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 14:52:28 -0800 Subject: [PATCH 05/42] fix publish_at --- src/Revisions/Revision.php | 10 ++-------- src/Revisions/RevisionRepository.php | 9 ++++++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index 8e3955fd387..ce3aa1c1b04 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -65,13 +65,7 @@ public function message($message = null) public function publishAt($dateTime = null) { - if (is_null($dateTime)) { - return $this; - } - - $carbon = new Carbon($dateTime['date'].' '.$dateTime['time'] ?? '00:00'); - - return $this->fluentlyGetOrSet('publishAt')->value($carbon->timestamp); + return $this->fluentlyGetOrSet('publishAt')->value($dateTime); } public function attributes($attributes = null) @@ -116,7 +110,7 @@ public function fileData() 'date' => $this->date->timestamp, 'user' => $this->userId ?: null, 'message' => $this->message ?: null, - 'publish_at' => $this->publishAt ?: null, + 'publish_at' => $this->publishAt?->timestamp, 'attributes' => $this->attributes, ]; } diff --git a/src/Revisions/RevisionRepository.php b/src/Revisions/RevisionRepository.php index 88641f3268e..3e4cde02bba 100644 --- a/src/Revisions/RevisionRepository.php +++ b/src/Revisions/RevisionRepository.php @@ -8,6 +8,7 @@ use Statamic\Facades\File; use Statamic\Facades\Folder; use Statamic\Facades\YAML; +use Statamic\Support\Arr; use Statamic\Support\FileCollection; use Statamic\Support\Str; @@ -67,7 +68,7 @@ protected function makeRevisionFromFile($key, $path) { $yaml = YAML::parse(File::get($path)); - return (new Revision) + $revision = (new Revision) ->key($key) ->action($yaml['action'] ?? false) ->id($date = $yaml['date']) @@ -75,5 +76,11 @@ protected function makeRevisionFromFile($key, $path) ->user($yaml['user'] ?? false) ->message($yaml['message'] ?? false) ->attributes($yaml['attributes']); + + if (! is_null($timestamp = Arr::get($yaml, 'publish_at'))) { + $revision->publishAt(Carbon::createFromTimestamp($timestamp)); + } + + return $revision; } } From aeec914729c20ba630d643b645fad03489df6dab Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 14:52:37 -0800 Subject: [PATCH 06/42] hasRevisions --- src/Revisions/Revisable.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Revisions/Revisable.php b/src/Revisions/Revisable.php index 8cc125ac760..0ff64f30a30 100644 --- a/src/Revisions/Revisable.php +++ b/src/Revisions/Revisable.php @@ -9,6 +9,11 @@ trait Revisable { + public function hasRevisions(): bool + { + return $this->revisions()->isNotEmpty(); + } + public function revision(string $reference) { return $this->revisions()->get($reference); From 5837d5e80abf28fbacff159869b3e6f8016fe935 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 14:52:58 -0800 Subject: [PATCH 07/42] get all the revisions that are ready to be published --- .../MinuteScheduledRevisionEntries.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/Entries/MinuteScheduledRevisionEntries.php diff --git a/src/Entries/MinuteScheduledRevisionEntries.php b/src/Entries/MinuteScheduledRevisionEntries.php new file mode 100644 index 00000000000..47b40654b35 --- /dev/null +++ b/src/Entries/MinuteScheduledRevisionEntries.php @@ -0,0 +1,25 @@ +filter(fn (Entry $entry) => $entry->hasRevisions()) + ->filter(fn (Entry $entry) => $entry->latestRevision()->publishAt()) + ->filter( + fn (Entry $entry) => $entry + ->latestRevision() + ->publishAt() + ->isSameMinute($this->minute) + ); + } +} From 9f8dccdf0b78f568b3d38fc60bfb078846bf53b6 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 15:05:21 -0800 Subject: [PATCH 08/42] not used --- src/Revisions/Revision.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index ce3aa1c1b04..28d6b1dc9ed 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -3,7 +3,6 @@ namespace Statamic\Revisions; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Support\Carbon; use Statamic\Contracts\Auth\User; use Statamic\Contracts\Revisions\Revision as Contract; use Statamic\Data\ExistsAsFile; From 9a0bfa25124a7dde8f3fa8eb00081294fe3a31f8 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 15:19:59 -0800 Subject: [PATCH 09/42] lint --- src/Entries/MinuteScheduledRevisionEntries.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Entries/MinuteScheduledRevisionEntries.php b/src/Entries/MinuteScheduledRevisionEntries.php index 47b40654b35..2fc424e42cb 100644 --- a/src/Entries/MinuteScheduledRevisionEntries.php +++ b/src/Entries/MinuteScheduledRevisionEntries.php @@ -8,7 +8,9 @@ class MinuteScheduledRevisionEntries { - public function __construct(private readonly CarbonInterface $minute) {} + public function __construct(private readonly CarbonInterface $minute) + { + } public function __invoke(): Collection { From ed1fcf905e449d295eef969b5940a76b42880bb3 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 15:44:58 -0800 Subject: [PATCH 10/42] fluent --- src/Revisions/Revisable.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Revisions/Revisable.php b/src/Revisions/Revisable.php index 0ff64f30a30..f2211eb6ddc 100644 --- a/src/Revisions/Revisable.php +++ b/src/Revisions/Revisable.php @@ -167,7 +167,7 @@ public function store($options = []) return $return; } - public function createRevision($options = []) + public function createRevision($options = []): self { $this ->fromWorkingCopy() @@ -176,6 +176,8 @@ public function createRevision($options = []) ->message($options['message'] ?? false) ->publishAt($options['publish_at'] ?? null) ->save(); + + return $this; } public function revisionsEnabled() From 05546b12ae5179e303f0b2615dcad82f18be5ab0 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 15:45:14 -0800 Subject: [PATCH 11/42] needs to be carbon --- .../CP/Collections/EntryRevisionsController.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Http/Controllers/CP/Collections/EntryRevisionsController.php b/src/Http/Controllers/CP/Collections/EntryRevisionsController.php index 7f69004056e..236face8f0b 100644 --- a/src/Http/Controllers/CP/Collections/EntryRevisionsController.php +++ b/src/Http/Controllers/CP/Collections/EntryRevisionsController.php @@ -3,6 +3,7 @@ namespace Statamic\Http\Controllers\CP\Collections; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Statamic\Facades\Site; use Statamic\Facades\User; use Statamic\Http\Controllers\CP\CpController; @@ -39,13 +40,16 @@ public function index(Request $request, $collection, $entry) public function store(Request $request, $collection, $entry) { - $entry->createRevision([ + $data = [ 'message' => $request->message, - 'publish_at' => $request->publish_at, 'user' => User::fromUser($request->user()), - ]); + ]; + + if (! is_null($dateTime = $request->publish_at)) { + $data['publish_at'] = Carbon::parse($dateTime['date'].' '.$dateTime['time'] ?? '00:00'); + } - return new EntryResource($entry); + return new EntryResource($entry->createRevision($data)); } public function show(Request $request, $collection, $entry, $revision) From d6b7dc495a164ded84f68b88215a61ce6e8541d7 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 16:03:03 -0800 Subject: [PATCH 12/42] wip --- src/Jobs/HandleRevisionSchedule.php | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/Jobs/HandleRevisionSchedule.php diff --git a/src/Jobs/HandleRevisionSchedule.php b/src/Jobs/HandleRevisionSchedule.php new file mode 100644 index 00000000000..ead26beeb21 --- /dev/null +++ b/src/Jobs/HandleRevisionSchedule.php @@ -0,0 +1,36 @@ +entries()->each(function (Entry $entry) { + $updatedEntry = tap($entry->makeFromRevision($entry->latestRevision()))->save(); + EntryScheduleReached::dispatch($updatedEntry); + }); + } + + private function entries(): Collection + { + // We want to target the PREVIOUS minute because we can be sure that any entries that + // were scheduled for then would now be considered published. If we were targeting + // the current minute and the entry has defined a time with seconds later in the + // same minute, it may still be considered scheduled when it gets dispatched. + $minute = now()->subMinute(); + + return (new MinuteScheduledRevisionEntries($minute))(); + } +} From b039f5c49387e1b5522b8a51d6c9376badd94197 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 27 Feb 2025 16:18:39 -0800 Subject: [PATCH 13/42] is this better? --- src/Contracts/Revisions/Revision.php | 4 ++++ ...onEntries.php => MinuteScheduledRevisions.php} | 2 +- src/Jobs/HandleRevisionSchedule.php | 15 +++++++++------ src/Revisions/Revision.php | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) rename src/Entries/{MinuteScheduledRevisionEntries.php => MinuteScheduledRevisions.php} (94%) diff --git a/src/Contracts/Revisions/Revision.php b/src/Contracts/Revisions/Revision.php index 12361213275..73b3cc336a8 100644 --- a/src/Contracts/Revisions/Revision.php +++ b/src/Contracts/Revisions/Revision.php @@ -2,8 +2,12 @@ namespace Statamic\Contracts\Revisions; +use Statamic\Revisions\Revisable; + interface Revision { + public function currentContent(): Revisable; + public function id($id = null); public function message($message = null); diff --git a/src/Entries/MinuteScheduledRevisionEntries.php b/src/Entries/MinuteScheduledRevisions.php similarity index 94% rename from src/Entries/MinuteScheduledRevisionEntries.php rename to src/Entries/MinuteScheduledRevisions.php index 2fc424e42cb..605e9b2a5b0 100644 --- a/src/Entries/MinuteScheduledRevisionEntries.php +++ b/src/Entries/MinuteScheduledRevisions.php @@ -6,7 +6,7 @@ use Illuminate\Support\Collection; use Statamic\Facades\Entry as EntryFacade; -class MinuteScheduledRevisionEntries +class MinuteScheduledRevisions { public function __construct(private readonly CarbonInterface $minute) { diff --git a/src/Jobs/HandleRevisionSchedule.php b/src/Jobs/HandleRevisionSchedule.php index ead26beeb21..43bb4a548e8 100644 --- a/src/Jobs/HandleRevisionSchedule.php +++ b/src/Jobs/HandleRevisionSchedule.php @@ -8,8 +8,9 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Collection; use Statamic\Entries\Entry; -use Statamic\Entries\MinuteScheduledRevisionEntries; +use Statamic\Entries\MinuteScheduledRevisions; use Statamic\Events\EntryScheduleReached; +use Statamic\Revisions\Revision; class HandleRevisionSchedule implements ShouldQueue { @@ -17,13 +18,15 @@ class HandleRevisionSchedule implements ShouldQueue public function handle() { - $this->entries()->each(function (Entry $entry) { - $updatedEntry = tap($entry->makeFromRevision($entry->latestRevision()))->save(); - EntryScheduleReached::dispatch($updatedEntry); + $this->revisions()->each(function (Revision $revision) { + $existingContent = $revision->currentContent(); + + $updatedContent = tap($existingContent->makeFromRevision($revision))->save(); + // EntryScheduleReached::dispatch($updatedEntry); }); } - private function entries(): Collection + private function revisions(): Collection { // We want to target the PREVIOUS minute because we can be sure that any entries that // were scheduled for then would now be considered published. If we were targeting @@ -31,6 +34,6 @@ private function entries(): Collection // same minute, it may still be considered scheduled when it gets dispatched. $minute = now()->subMinute(); - return (new MinuteScheduledRevisionEntries($minute))(); + return (new MinuteScheduledRevisions($minute))(); } } diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index 28d6b1dc9ed..a03708b363b 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -10,6 +10,7 @@ use Statamic\Events\RevisionSaved; use Statamic\Events\RevisionSaving; use Statamic\Facades; +use Statamic\Facades\Data; use Statamic\Facades\Revision as Revisions; use Statamic\Support\Traits\FluentlyGetsAndSets; @@ -18,15 +19,28 @@ class Revision implements Arrayable, Contract use ExistsAsFile, FluentlyGetsAndSets; protected $id; + protected $key; + protected $date; + protected $user; + protected $userId; + protected $message; + protected $publishAt; + protected $action = 'revision'; + protected $attributes = []; + public function currentContent(): Revisable + { + return Data::find($this->attribute('id')); + } + public function id($id = null) { return $this->fluentlyGetOrSet('id')->value($id); From 630281a1a2ab4eb36fc74641ad76ed1320bd5cfb Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 28 Feb 2025 11:10:57 -0800 Subject: [PATCH 14/42] tests --- src/Contracts/Revisions/Revision.php | 4 +- src/Revisions/Revision.php | 8 +-- tests/Feature/Revisions/RevisableTest.php | 68 +++++++++++++++++++ tests/Feature/Revisions/RevisionsTest.php | 52 ++++++++++++++ .../__fixtures__/123/1553546421.yaml | 3 + .../__fixtures__/123/1553546422.yaml | 4 ++ .../Revisions/__fixtures__/123/working.yaml | 3 + tests/Revisions/RepositoryTest.php | 8 +++ 8 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/Revisions/RevisableTest.php create mode 100644 tests/Feature/Revisions/__fixtures__/123/1553546421.yaml create mode 100644 tests/Feature/Revisions/__fixtures__/123/1553546422.yaml create mode 100644 tests/Feature/Revisions/__fixtures__/123/working.yaml diff --git a/src/Contracts/Revisions/Revision.php b/src/Contracts/Revisions/Revision.php index 73b3cc336a8..7ccc0ac5bd9 100644 --- a/src/Contracts/Revisions/Revision.php +++ b/src/Contracts/Revisions/Revision.php @@ -2,11 +2,9 @@ namespace Statamic\Contracts\Revisions; -use Statamic\Revisions\Revisable; - interface Revision { - public function currentContent(): Revisable; + public function currentContent(); public function id($id = null); diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index a03708b363b..69aadc3ea89 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -10,7 +10,7 @@ use Statamic\Events\RevisionSaved; use Statamic\Events\RevisionSaving; use Statamic\Facades; -use Statamic\Facades\Data; +use Statamic\Facades\Entry; use Statamic\Facades\Revision as Revisions; use Statamic\Support\Traits\FluentlyGetsAndSets; @@ -36,9 +36,9 @@ class Revision implements Arrayable, Contract protected $attributes = []; - public function currentContent(): Revisable + public function currentContent() { - return Data::find($this->attribute('id')); + return Entry::find($this->attribute('id')); } public function id($id = null) @@ -78,7 +78,7 @@ public function message($message = null) public function publishAt($dateTime = null) { - return $this->fluentlyGetOrSet('publishAt')->value($dateTime); + return $this->fluentlyGetOrSet('publishAt')->args(func_get_args()); } public function attributes($attributes = null) diff --git a/tests/Feature/Revisions/RevisableTest.php b/tests/Feature/Revisions/RevisableTest.php new file mode 100644 index 00000000000..84fb07f0ba3 --- /dev/null +++ b/tests/Feature/Revisions/RevisableTest.php @@ -0,0 +1,68 @@ + [ + 'enabled' => true, + 'path' => __DIR__.'/__fixtures__', + ]]); + + $this->revisable = new class + { + use Revisable; + + protected function revisionKey() + { + return '123'; + } + + protected function revisionAttributes() + { + return [ + 'id' => 123 + ]; + } + + public function makeFromRevision($revision) + { + return new self; + } + }; + } + + #[Test] + public function has_revisions() + { + $this->assertTrue($this->revisable->hasRevisions()); + } + + #[Test] + public function sets_publish_at_from_options() + { + Carbon::setTestNow($now = now()); + + $this->revisable->createRevision(['publish_at' => $now]); + + $revision = $this->revisable->latestRevision(); + Revision::delete($revision); + + $this->assertEquals($revision->publishAt()->timestamp, $now->timestamp); + } +} diff --git a/tests/Feature/Revisions/RevisionsTest.php b/tests/Feature/Revisions/RevisionsTest.php index 50b003faab8..457c0ba68da 100644 --- a/tests/Feature/Revisions/RevisionsTest.php +++ b/tests/Feature/Revisions/RevisionsTest.php @@ -60,4 +60,56 @@ public function a_revision_can_be_made_from_a_dated_entry() $this->assertEquals(['id' => '123', 'published' => true, 'slug' => 'my-entry', 'data' => ['foo' => 'bar'], 'date' => '1482624000'], $revision->attributes()); } + + #[Test] + public function can_get_its_entry() + { + config(['statamic.revisions.path' => '/path/to']); + + Carbon::setTestNow($now = Carbon::parse('2019-03-25 13:15')); + + $entry = EntryFactory::id('123') + ->collection(tap(Collection::make('test')->dated(true))->save()) + ->slug('my-entry') + ->data(['foo' => 'bar']) + ->date('2016-12-25') + ->make(); + + $entry->save(); + $revision = $entry->makeRevision(); + + $this->assertEquals($entry->id(), $revision->currentContent()->id()); + } + + #[Test] + public function converts_publish_at_to_timestamp_when_saving() + { + Carbon::setTestNow(now()); + + $revision = (new Revision) + ->date(now()) + ->publishAt(now()); + + $this->assertEquals(now()->timestamp, $revision->fileData()['publish_at']); + } + + #[Test] + public function converts_publish_at_to_null_when_saving() + { + $revision = (new Revision)->date(now()); + + $this->assertNull($revision->fileData()['publish_at']); + } + + #[Test] + public function outputs_publish_at_when_to_array() + { + Carbon::setTestNow(now()); + + $revision = (new Revision) + ->date(now()) + ->publishAt(now()); + + $this->assertEquals(now(), $revision->toArray()['publish_at']); + } } diff --git a/tests/Feature/Revisions/__fixtures__/123/1553546421.yaml b/tests/Feature/Revisions/__fixtures__/123/1553546421.yaml new file mode 100644 index 00000000000..a19607e74a2 --- /dev/null +++ b/tests/Feature/Revisions/__fixtures__/123/1553546421.yaml @@ -0,0 +1,3 @@ +date: 1553546421 +attributes: + date: 1740528000 diff --git a/tests/Feature/Revisions/__fixtures__/123/1553546422.yaml b/tests/Feature/Revisions/__fixtures__/123/1553546422.yaml new file mode 100644 index 00000000000..e196e528ae6 --- /dev/null +++ b/tests/Feature/Revisions/__fixtures__/123/1553546422.yaml @@ -0,0 +1,4 @@ +date: 1553546422 +publish_at: 1553644800 +attributes: + date: 1740528000 diff --git a/tests/Feature/Revisions/__fixtures__/123/working.yaml b/tests/Feature/Revisions/__fixtures__/123/working.yaml new file mode 100644 index 00000000000..c7e919a4893 --- /dev/null +++ b/tests/Feature/Revisions/__fixtures__/123/working.yaml @@ -0,0 +1,3 @@ +date: 1553546423 +attributes: + id: 123 diff --git a/tests/Revisions/RepositoryTest.php b/tests/Revisions/RepositoryTest.php index aa5a408fb48..9fbc53aa8c5 100644 --- a/tests/Revisions/RepositoryTest.php +++ b/tests/Revisions/RepositoryTest.php @@ -36,4 +36,12 @@ public function it_can_call_to_array_on_a_revision_collection() $this->assertIsArray($revisions->toArray()); } + + #[Test] + public function it_loads_publish_at_as_carbon() + { + $revision = $this->repo->whereKey('123')->last(); + + $this->assertEquals(1553644800, $revision->publishAt()->timestamp); + } } From 55835fbb4d8082ac960d2818d4186d5d207a42a4 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 28 Feb 2025 11:49:19 -0800 Subject: [PATCH 15/42] =?UTF-8?q?gettin=E2=80=99=20there?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Feature/Revisions/RevisableTest.php | 2 +- tests/Revisions/__fixtures__/123/1553546422.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Revisions/RevisableTest.php b/tests/Feature/Revisions/RevisableTest.php index 84fb07f0ba3..9d46d91cf11 100644 --- a/tests/Feature/Revisions/RevisableTest.php +++ b/tests/Feature/Revisions/RevisableTest.php @@ -36,7 +36,7 @@ protected function revisionKey() protected function revisionAttributes() { return [ - 'id' => 123 + 'id' => 123, ]; } diff --git a/tests/Revisions/__fixtures__/123/1553546422.yaml b/tests/Revisions/__fixtures__/123/1553546422.yaml index af3b65bb764..7a1669b18d1 100644 --- a/tests/Revisions/__fixtures__/123/1553546422.yaml +++ b/tests/Revisions/__fixtures__/123/1553546422.yaml @@ -1,2 +1,3 @@ date: 1553546422 +publish_at: 1553644800 attributes: {} From ba71eb5eaf0ada864bf57539a738fdbc7aeb11b9 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 28 Feb 2025 12:00:11 -0800 Subject: [PATCH 16/42] add test --- tests/Feature/Entries/EntryRevisionsTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Entries/EntryRevisionsTest.php b/tests/Feature/Entries/EntryRevisionsTest.php index d9d032ead25..6d76398d062 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -245,7 +245,7 @@ public function it_creates_a_revision() $this ->actingAs($user) - ->post($entry->createRevisionUrl(), ['message' => 'Test!']) + ->post($entry->createRevisionUrl(), ['message' => 'Test!', 'publish_at' => ['date' => '2010-12-29', 'time' => '11:00am']]) ->assertOk(); $entry = Entry::find($entry->id()); @@ -270,6 +270,7 @@ public function it_creates_a_revision() ], $revision->attributes()); $this->assertEquals('user-1', $revision->user()->id()); $this->assertEquals('Test!', $revision->message()); + $this->assertEquals(1293620400, $revision->publishAt()->timestamp); $this->assertEquals('revision', $revision->action()); $this->assertTrue($entry->hasWorkingCopy()); } From f70b8cb2a8240150ea912b70b30145b9ea6deee9 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 28 Feb 2025 14:50:47 -0800 Subject: [PATCH 17/42] more tests --- ...duledRevisions.php => MinuteRevisions.php} | 2 +- tests/Data/Entries/ScheduledRevisionsTest.php | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) rename src/Entries/{MinuteScheduledRevisions.php => MinuteRevisions.php} (95%) create mode 100644 tests/Data/Entries/ScheduledRevisionsTest.php diff --git a/src/Entries/MinuteScheduledRevisions.php b/src/Entries/MinuteRevisions.php similarity index 95% rename from src/Entries/MinuteScheduledRevisions.php rename to src/Entries/MinuteRevisions.php index 605e9b2a5b0..58aef89f03c 100644 --- a/src/Entries/MinuteScheduledRevisions.php +++ b/src/Entries/MinuteRevisions.php @@ -6,7 +6,7 @@ use Illuminate\Support\Collection; use Statamic\Facades\Entry as EntryFacade; -class MinuteScheduledRevisions +class MinuteRevisions { public function __construct(private readonly CarbonInterface $minute) { diff --git a/tests/Data/Entries/ScheduledRevisionsTest.php b/tests/Data/Entries/ScheduledRevisionsTest.php new file mode 100644 index 00000000000..db76a85aa92 --- /dev/null +++ b/tests/Data/Entries/ScheduledRevisionsTest.php @@ -0,0 +1,67 @@ +dir = __DIR__.'/tmp'; + config(['statamic.revisions.enabled' => true]); + config(['statamic.revisions.path' => $this->dir]); + } + + public function tearDown(): void + { + File::deleteDirectory($this->dir); + parent::tearDown(); + } + + #[Test] + public function it_gets_entries_scheduled_for_given_minute() + { + Carbon::setTestNow($now = now()->setSeconds(2)->toImmutable()); + + Collection::make('revisable')->revisionsEnabled(true)->save(); + + EntryFactory::id(1) + ->collection('revisable') + ->create() + ->makeRevision() + ->publishAt($now->addSeconds(5)) + ->save(); + + EntryFactory::id(2) + ->collection('revisable') + ->create() + ->makeRevision() + ->save(); + + EntryFactory::id(3) + ->collection('revisable') + ->create() + ->makeRevision() + ->publishAt($now->addSeconds(65)) + ->save(); + + $this->assertEquals([1], $this->getRevisionsForMinute($now)); + $this->assertEmpty($this->getRevisionsForMinute($now->addMinutes(2))); + } + + private function getRevisionsForMinute($minute) + { + return (new MinuteRevisions($minute))()->map->id()->all(); + } +} From 36b4df616acd94582882170a2805fe121467fa4c Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 28 Feb 2025 14:57:47 -0800 Subject: [PATCH 18/42] tidy --- src/Contracts/Revisions/Revision.php | 4 +++- src/Jobs/HandleRevisionSchedule.php | 16 +++++++--------- src/Providers/AppServiceProvider.php | 6 +++++- src/Revisions/Revision.php | 7 ++++--- tests/Feature/Revisions/RevisionsTest.php | 2 +- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Contracts/Revisions/Revision.php b/src/Contracts/Revisions/Revision.php index 7ccc0ac5bd9..53e4988c4d9 100644 --- a/src/Contracts/Revisions/Revision.php +++ b/src/Contracts/Revisions/Revision.php @@ -2,9 +2,11 @@ namespace Statamic\Contracts\Revisions; +use Statamic\Entries\Entry; + interface Revision { - public function currentContent(); + public function entry(): Entry; public function id($id = null); diff --git a/src/Jobs/HandleRevisionSchedule.php b/src/Jobs/HandleRevisionSchedule.php index 43bb4a548e8..5ae2583cc1b 100644 --- a/src/Jobs/HandleRevisionSchedule.php +++ b/src/Jobs/HandleRevisionSchedule.php @@ -7,8 +7,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Collection; -use Statamic\Entries\Entry; -use Statamic\Entries\MinuteScheduledRevisions; +use Statamic\Entries\MinuteRevisions; use Statamic\Events\EntryScheduleReached; use Statamic\Revisions\Revision; @@ -18,12 +17,11 @@ class HandleRevisionSchedule implements ShouldQueue public function handle() { - $this->revisions()->each(function (Revision $revision) { - $existingContent = $revision->currentContent(); - - $updatedContent = tap($existingContent->makeFromRevision($revision))->save(); - // EntryScheduleReached::dispatch($updatedEntry); - }); + $this + ->revisions() + ->each(fn (Revision $revision) => EntryScheduleReached::dispatch( + tap($revision->entry()->makeFromRevision($revision))->save() + )); } private function revisions(): Collection @@ -34,6 +32,6 @@ private function revisions(): Collection // same minute, it may still be considered scheduled when it gets dispatched. $minute = now()->subMinute(); - return (new MinuteScheduledRevisions($minute))(); + return (new MinuteRevisions($minute))(); } } diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index ada744379f5..69cd942a185 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -17,6 +17,7 @@ use Statamic\Facades\Token; use Statamic\Fields\FieldsetRecursionStack; use Statamic\Jobs\HandleEntrySchedule; +use Statamic\Jobs\HandleRevisionSchedule; use Statamic\Sites\Sites; use Statamic\Statamic; use Statamic\Tokens\Handlers\LivePreview; @@ -104,7 +105,10 @@ public function boot() $this->addAboutCommandInfo(); - $this->app->make(Schedule::class)->job(new HandleEntrySchedule)->everyMinute(); + tap($this->app->make(Schedule::class), function (Schedule $scheduler) { + $scheduler->job(new HandleEntrySchedule)->everyMinute(); + $scheduler->job(new HandleRevisionSchedule)->everyMinute(); + }); } public function register() diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index 69aadc3ea89..340fe240d69 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -6,11 +6,12 @@ use Statamic\Contracts\Auth\User; use Statamic\Contracts\Revisions\Revision as Contract; use Statamic\Data\ExistsAsFile; +use Statamic\Entries\Entry; use Statamic\Events\RevisionDeleted; use Statamic\Events\RevisionSaved; use Statamic\Events\RevisionSaving; use Statamic\Facades; -use Statamic\Facades\Entry; +use Statamic\Facades\Entry as EntryFacade; use Statamic\Facades\Revision as Revisions; use Statamic\Support\Traits\FluentlyGetsAndSets; @@ -36,9 +37,9 @@ class Revision implements Arrayable, Contract protected $attributes = []; - public function currentContent() + public function entry(): Entry { - return Entry::find($this->attribute('id')); + return EntryFacade::find($this->attribute('id')); } public function id($id = null) diff --git a/tests/Feature/Revisions/RevisionsTest.php b/tests/Feature/Revisions/RevisionsTest.php index 457c0ba68da..31a3057059a 100644 --- a/tests/Feature/Revisions/RevisionsTest.php +++ b/tests/Feature/Revisions/RevisionsTest.php @@ -78,7 +78,7 @@ public function can_get_its_entry() $entry->save(); $revision = $entry->makeRevision(); - $this->assertEquals($entry->id(), $revision->currentContent()->id()); + $this->assertEquals($entry->id(), $revision->entry()->id()); } #[Test] From 35869c23505dfcf926e9cd5f8b6edf69621c7705 Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 14:51:12 -0700 Subject: [PATCH 19/42] use v6 version --- resources/lang/en/messages.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php index f89565b22e9..c4c3e42b31a 100644 --- a/resources/lang/en/messages.php +++ b/resources/lang/en/messages.php @@ -189,9 +189,8 @@ 'preference_start_page_instructions' => 'The page to be shown when logging into the control panel.', 'publish_actions_create_revision' => 'A revision will be created based off the working copy. The current revision will not change.', 'publish_actions_current_becomes_draft_because_scheduled' => 'Since the current revision is published and you\'ve selected a date in the future, once you submit, the revision will act like a draft until the selected date.', - 'publish_actions_publish' => 'Changes to the working copy will applied to the entry and it will be published immediately.', - 'publish_actions_schedule' => 'Changes to the working copy will applied to the entry and it will be appear published on the selected date.', - 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', + 'publish_actions_publish' => 'Changes to the working copy will be applied to the entry and it will be published immediately.', + 'publish_actions_schedule' => 'Changes to the working copy will be applied to the entry and it will appear published on the selected date.', 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', 'publish_actions_unpublish' => 'The current revision will be unpublished.', 'reset_password_notification_body' => 'You are receiving this email because we received a password reset request for your account.', 'reset_password_notification_no_action' => 'If you did not request a password reset, no further action is required.', From 6fb7cc84c4e482890cd41a541bb3585acb8ac6e5 Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 14:53:33 -0700 Subject: [PATCH 20/42] v6 version --- lang/en/messages.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/en/messages.php b/lang/en/messages.php index 6a6f6980f3a..9dba85a638f 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -204,8 +204,8 @@ 'preference_theme_instructions' => 'Customize the colors and appearance of the control panel.', 'publish_actions_create_revision' => 'A revision will be created based on the working copy. The current revision will not change.', 'publish_actions_current_becomes_draft_because_scheduled' => 'Since the current revision is published and you\'ve selected a date in the future, once you submit, the revision will act like a draft until the selected date.', - 'publish_actions_publish' => 'Changes to the working copy will applied to the entry and it will be published immediately.', - 'publish_actions_schedule' => 'Changes to the working copy will applied to the entry and it will be appear published on the selected date.', + 'publish_actions_publish' => 'Changes to the working copy will be applied to the entry and it will be published immediately.', + 'publish_actions_schedule' => 'Changes to the working copy will be applied to the entry and it will appear published on the selected date.', 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', 'publish_actions_unpublish' => 'The current revision will be unpublished.', 'reset_password_notification_body' => 'You are receiving this email because we received a password reset request for your account.', From 248343089fbb74229a68daf3aad3d17818ddc181 Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 14:54:10 -0700 Subject: [PATCH 21/42] whoops --- lang/en/messages.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/messages.php b/lang/en/messages.php index 9dba85a638f..23c714daaf4 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -205,7 +205,7 @@ 'publish_actions_create_revision' => 'A revision will be created based on the working copy. The current revision will not change.', 'publish_actions_current_becomes_draft_because_scheduled' => 'Since the current revision is published and you\'ve selected a date in the future, once you submit, the revision will act like a draft until the selected date.', 'publish_actions_publish' => 'Changes to the working copy will be applied to the entry and it will be published immediately.', - 'publish_actions_schedule' => 'Changes to the working copy will be applied to the entry and it will appear published on the selected date.', 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', + 'publish_actions_schedule' => 'Changes to the working copy will be applied to the entry and it will appear published on the selected date.', 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', 'publish_actions_unpublish' => 'The current revision will be unpublished.', 'reset_password_notification_body' => 'You are receiving this email because we received a password reset request for your account.', From 5cf91b67815ae24f73c20b9e23fc6119750ddb4e Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 14:55:15 -0700 Subject: [PATCH 22/42] =?UTF-8?q?it=E2=80=99s=20elsewhere=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/lang/en/messages.php | 258 --------------------------------- 1 file changed, 258 deletions(-) delete mode 100644 resources/lang/en/messages.php diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php deleted file mode 100644 index c4c3e42b31a..00000000000 --- a/resources/lang/en/messages.php +++ /dev/null @@ -1,258 +0,0 @@ - 'You are receiving this email because we received a password reset request for your account.', - 'activate_account_notification_subject' => 'Activate Your Account', - 'addon_has_more_releases_beyond_license_body' => 'You may update, but will need to upgrade or purchase a new license.', - 'addon_has_more_releases_beyond_license_heading' => 'This addon has more releases beyond your licensed limit.', - 'addon_install_command' => 'To install this addon, run the following command', - 'addon_list_loading_error' => 'Something went wrong while loading addons. Try again later.', - 'addon_uninstall_command' => 'To uninstall this addon, run the following command', - 'asset_container_allow_uploads_instructions' => 'When enabled will give users the ability upload files into this container.', - 'asset_container_blueprint_instructions' => 'Blueprints define additional custom fields available when editing assets.', - 'asset_container_create_folder_instructions' => 'When enabled will give users the ability to create folders in this container.', - 'asset_container_disk_instructions' => 'Filesystem disks specify where files are stored — either locally or in a remote location like Amazon S3. They can be configured in `config/filesystems.php`', - 'asset_container_handle_instructions' => 'Used to reference this container on the frontend. It\'s non-trivial to change later.', - 'asset_container_intro' => 'Media and document files live in folders on the server or other file storage services. Each of these locations is called a container.', - 'asset_container_move_instructions' => 'When enabled will allow users to move files around inside this container.', - 'asset_container_quick_download_instructions' => 'When enabled will add a quick download button in the Asset Manager.', - 'asset_container_rename_instructions' => 'When enabled will allow users to rename the files in this container.', - 'asset_container_source_preset_instructions' => 'Uploaded images will be permanently processed using this preset.', - 'asset_container_title_instructions' => 'Usually a plural noun, like Images or Documents', - 'asset_container_validation_rules_instructions' => 'These rules will be applied to uploaded files.', - 'asset_container_warm_intelligent_instructions' => 'Generate appropriate presets on upload.', - 'asset_container_warm_presets_instructions' => 'Specify which presets to generate on upload.', - 'asset_folders_directory_instructions' => 'We recommend avoiding spaces and special characters to keep URLs clean.', - 'asset_replace_confirmation' => 'References to this asset within content will be updated to the asset you select below.', - 'asset_reupload_confirmation' => 'Are you sure you want to reupload this asset?', - 'asset_reupload_warning' => 'You may encounter browser or server-level caching issues. You may prefer to replace the asset instead.', - 'blueprints_hidden_instructions' => 'Hides the blueprint from the create buttons in the CP', - 'blueprints_intro' => 'Blueprints define and organize fields to create the content models for collections, forms, and other data types.', - 'blueprints_title_instructions' => 'Usually a singular noun, like Article or Product', - 'cache_utility_application_cache_description' => 'Laravel\'s unified cache used by Statamic, third party addons, and composer packages.', - 'cache_utility_description' => 'Manage and view important information about Statamic\'s various caching layers.', - 'cache_utility_image_cache_description' => 'The image cache stores copies of all transformed and resized images.', - 'cache_utility_stache_description' => 'The Stache is Statamic\'s content store that functions much like a database. It is generated automatically from the content files.', - 'cache_utility_static_cache_description' => 'Static pages bypass Statamic completely and are rendered directly from the server for maximum performance.', - 'choose_entry_localization_deletion_behavior' => 'Choose the action you wish to perform on the localized entries.', - 'collection_configure_date_behavior_private' => 'Private - Hidden from listings, URLs 404', - 'collection_configure_date_behavior_public' => 'Public - Always visible', - 'collection_configure_date_behavior_unlisted' => 'Unlisted - Hidden from listings, URLs visible', - 'collection_configure_dated_instructions' => 'Publish dates can be used to schedule and expire content.', - 'collection_configure_handle_instructions' => 'Used to reference this collection on the frontend. It\'s non-trivial to change later.', - 'collection_configure_intro' => 'Collections are containers that hold entries that can represent articles, blog posts, products, events, or any other content type.', - 'collection_configure_layout_instructions' => 'Set this collection\'s default layout. Entries can override this setting with a `template` field named `layout`. It is unusual to change this setting.', - 'collection_configure_origin_behavior_instructions' => 'When localizing an entry, which site should be used as the origin?', - 'collection_configure_origin_behavior_option_active' => 'Use the active site of the entry being edited', - 'collection_configure_origin_behavior_option_root' => 'Use the site the entry was originally created in', - 'collection_configure_origin_behavior_option_select' => 'Let the user select the origin', - 'collection_configure_propagate_instructions' => 'Automatically propagate new entries to all configured sites.', - 'collection_configure_require_slugs_instructions' => 'Whether entries are required to have slugs.', - 'collection_configure_template_instructions' => 'Set this collection\'s default template. Entries can override this setting with a `template` field.', - 'collection_configure_title_format_instructions' => 'Set this to have the entries in this collection generate their titles automatically. Learn more in the [documentation](https://statamic.dev/collections#titles).', - 'collection_configure_title_instructions' => 'We recommend a plural noun, like "Articles" or "Products".', - 'collection_next_steps_blueprints_description' => 'Manage the blueprints and fields available for this collection.', - 'collection_next_steps_configure_description' => 'Configure URLs and routes, define blueprints, date behaviors, ordering and other options.', - 'collection_next_steps_create_entry_description' => 'Create the first entry or stub out a handful of placeholder entries, it\'s up to you.', - 'collection_next_steps_scaffold_description' => 'Quickly generate index and detail views from the name of the collection.', - 'collection_revisions_instructions' => 'Enable revisions for this Collection.', - 'collection_scaffold_instructions' => 'Choose which empty views to generate. Existing files will not be overwritten.', - 'collections_blueprint_instructions' => 'Entries in this collection may use any of these blueprints.', - 'collections_default_publish_state_instructions' => 'While creating new entries in this collection the published toggle will default to **true** instead of **false** (draft).', - 'collections_future_date_behavior_instructions' => 'How future dated entries should behave.', - 'collections_links_instructions' => 'Entries in this collection may contain links (redirects) to other entries or URLs.', - 'collections_mount_instructions' => 'Choose an entry on which this collection should be mounted. Learn more in the [documentation](https://statamic.dev/collections-and-entries#mounting).', - 'collections_orderable_instructions' => 'Enable manual ordering via drag & drop.', - 'collections_past_date_behavior_instructions' => 'How past dated entries should behave.', - 'collections_preview_target_refresh_instructions' => 'Automatically refresh the preview while editing. Disabling this will use postMessage.', - 'collections_preview_targets_instructions' => 'The URLs to be viewable within Live Preview. Learn more in the [documentation](https://statamic.dev/live-preview#preview-targets).', - 'collections_route_instructions' => 'The route controls entries URL pattern. Learn more in the [documentation](https://statamic.dev/collections#routing).', - 'collections_sort_direction_instructions' => 'The default sort direction.', - 'collections_taxonomies_instructions' => 'Connect entries in this collection to taxonomies. Fields will be automatically added to publish forms.', - 'dictionaries_countries_emojis_instructions' => 'Whether flag emojis should be included in the labels.', - 'dictionaries_countries_region_instructions' => 'Optionally filter the countries by region.', - 'duplicate_action_localizations_confirmation' => 'Are you sure you want to run this action? Localizations will also be duplicated.', - 'duplicate_action_warning_localization' => 'This entry is a localization. The origin entry will be duplicated.', - 'duplicate_action_warning_localizations' => 'One or more selected entries are localizations. In those cases, the origin entry will be duplicated instead.', - 'email_utility_configuration_description' => 'Mail settings are configured in :path', - 'email_utility_description' => 'Check email configuration settings and send test emails.', - 'entry_origin_instructions' => 'The new localization will inherit values from the entry in the selected site.', - 'expect_root_instructions' => 'Consider the first page in the tree a "root" or "home" page.', - 'field_conditions_always_save_instructions' => 'Always save field value, even if field is hidden.', - 'field_conditions_field_instructions' => 'You may enter any field handle. You are not limited to the options in the dropdown.', - 'field_conditions_instructions' => 'When to show or hide this field.', - 'field_desynced_from_origin' => 'Desynced from origin. Click to sync and revert to the origin\'s value.', - 'field_synced_with_origin' => 'Synced with origin. Click or edit the field to desync.', - 'field_validation_advanced_instructions' => 'Add more advanced validation to this field.', - 'field_validation_required_instructions' => 'Control whether or not this field is required.', - 'field_validation_sometimes_instructions' => 'Only validate when this field is visible or submitted.', - 'fields_blueprints_description' => 'Blueprints define the fields for content structures like collections, taxonomies, users, and forms.', - 'fields_default_instructions' => 'The value to be inserted whenever this field is empty on all publish forms.', - 'fields_display_instructions' => 'The field\'s label shown in the Control Panel.', - 'fields_duplicate_instructions' => 'Whether this field should be included when duplicating the item.', - 'fields_fieldsets_description' => 'Fieldsets are simple, flexible, and completely optional groupings of fields that help to organize reusable, pre-configured fields.', - 'fields_handle_instructions' => 'The field\'s template variable. Avoid using [reserved words](https://statamic.dev/tips/reserved-words#as-field-names) as handles.', - 'fields_instructions_instructions' => 'Provide additional field instructions like this very text. Markdown formatting is supported.', - '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_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.', - 'fieldset_import_prefix_instructions' => 'The prefix that should be applied to each field when they are imported. eg. hero_', - 'fieldset_intro' => 'Fieldsets are an optional companion to blueprints, acting as reusable partials that can be used within blueprints.', - 'fieldset_link_fields_prefix_instructions' => 'Every field in the linked fieldset will be prefixed with this. Useful if you want to import the same fields multiple times.', - 'fieldsets_handle_instructions' => 'Used to reference this fieldset elsewhere. It\'s non-trivial to change later.', - 'fieldsets_title_instructions' => 'Usually describes what fields will be inside, like Image Block or Meta Data', - 'filters_view_already_exists' => 'A view already exists with this name. Creating this view will overwrite the existing view with this name.', - 'focal_point_instructions' => 'Setting a focal point allows dynamic photo cropping with a subject that stays in frame.', - 'focal_point_previews_are_examples' => 'Crop previews are for example only', - 'forgot_password_enter_email' => 'Enter your email address so we can send a reset password link.', - 'form_configure_blueprint_instructions' => 'Choose from existing Blueprints or create a new one.', - 'form_configure_email_attachments_instructions' => 'Attach uploaded assets to this email.', - 'form_configure_email_bcc_instructions' => 'Email address of the BCC recipient(s) - comma separated.', - 'form_configure_email_cc_instructions' => 'Email address of the CC recipient(s) - comma separated.', - 'form_configure_email_from_instructions' => 'Leave blank to fall back to the site default', - 'form_configure_email_html_instructions' => 'The view for the html version of this email.', - 'form_configure_email_instructions' => 'Configure emails to be sent when new form submission are received.', - 'form_configure_email_markdown_instructions' => 'Render the HTML version of this email using markdown.', - 'form_configure_email_reply_to_instructions' => 'Leave blank to fall back to sender.', - 'form_configure_email_subject_instructions' => 'Email subject line.', - 'form_configure_email_text_instructions' => 'The view for the text version of this email.', - 'form_configure_email_to_instructions' => 'Email address of the recipient(s) - comma separated.', - 'form_configure_handle_instructions' => 'Used to reference this form on the frontend. It\'s non-trivial to change later.', - 'form_configure_honeypot_instructions' => 'Field name to use as a honeypot. Honeypots are special fields used to reduce botspam.', - 'form_configure_intro' => 'Forms are used to collect information from visitors and dispatch events and notifications when there are new submissions.', - 'form_configure_mailer_instructions' => 'Choose the mailer for sending this email. Leave blank to fall back to the default mailer.', - 'form_configure_store_instructions' => 'Disable to stop storing submissions. Events and email notifications will still be sent.', - 'form_configure_title_instructions' => 'Usually a call to action, like "Contact Us".', - 'getting_started_widget_blueprints' => 'Blueprints define the custom fields used to create and store content.', - 'getting_started_widget_collections' => 'Collections contain the different types of content in the site.', - 'getting_started_widget_docs' => 'Get to know Statamic by understanding its capabilities the right way.', - 'getting_started_widget_header' => 'Getting Started with Statamic', - 'getting_started_widget_intro' => 'To begin building your new Statamic site, we recommend starting with these steps.', - 'getting_started_widget_navigation' => 'Create multi-level lists of links that can be used to render navbars, footers, and so on.', - 'getting_started_widget_pro' => 'Statamic Pro adds unlimited user accounts, roles, permissions, git-integration, revisions, multi-site, and more!', - 'git_disabled' => 'Statamic Git integration is currently disabled.', - 'git_nothing_to_commit' => 'Nothing to commit, content paths clean!', - 'git_utility_description' => 'Manage Git tracked content.', - 'global_search_open_using_slash' => 'Focus global search using the / key', - 'global_set_config_intro' => 'Global Sets manage content available across the entire site, like company details, contact information, or front-end settings.', - 'global_set_no_fields_description' => 'You can add fields to the Blueprint, or you can manually add variables to the set itself.', - 'globals_blueprint_instructions' => 'Controls the fields to be displayed when editing the variables.', - 'globals_configure_handle_instructions' => 'Used to reference this global set on the frontend. It\'s non-trivial to change later.', - 'globals_configure_intro' => 'A global set is a group of variables available across all front-end pages.', - 'globals_configure_title_instructions' => 'We recommend a noun representing the set\'s contents. eg. "Brand" or "Company"', - 'impersonate_action_confirmation' => 'You will be logged in as this user. You can return to your account using the avatar menu.', - 'licensing_config_cached_warning' => 'Any changes you make to your .env or config files will not be detected until you clear the cache. If you are seeing unexpected licensing results here, it may be because of this. You can use the php artisan config:cache command to regenerate the cache.', - 'licensing_error_invalid_domain' => 'Invalid domain', - 'licensing_error_invalid_edition' => 'License is for :edition edition', - 'licensing_error_no_domains' => 'No domains defined', - 'licensing_error_no_site_key' => 'No site license key', - 'licensing_error_outside_license_range' => 'License valid for versions :start and :end', - 'licensing_error_unknown_site' => 'Unknown site', - 'licensing_error_unlicensed' => 'Unlicensed', - 'licensing_incorrect_key_format_body' => 'It appears that your site key is not in the correct format. Please check the key and try again. You can grab your site key from your account area on statamic.com. It is alphanumeric and 16 characters long. Make sure to not use the legacy license key which is a UUID.', - 'licensing_incorrect_key_format_heading' => 'Incorrect site key format', - 'licensing_production_alert' => 'This site is using Statamic Pro and commercial addons. Please purchase appropriate licenses.', - 'licensing_production_alert_addons' => 'This site is using commercial addons. Please purchase appropriate licenses.', - 'licensing_production_alert_renew_statamic' => 'Using this version of Statamic Pro requires a license renewal.', - 'licensing_production_alert_statamic' => 'This site is using Statamic Pro. Please purchase a license.', - 'licensing_sync_instructions' => 'Data from statamic.com is synced once per hour. Force a sync to see any changes you\'ve made.', - 'licensing_trial_mode_alert' => 'This site is using Statamic Pro and commercial addons. Make sure to buy licenses before launching. Thanks!', - 'licensing_trial_mode_alert_addons' => 'This site is using commercial addons. Make sure to buy licenses before launching. Thanks!', - 'licensing_trial_mode_alert_statamic' => 'This site is using Statamic Pro. Make sure to buy a license before launching. Thanks!', - 'licensing_utility_description' => 'View and resolve licensing details.', - 'max_depth_instructions' => 'Set a maximum number of levels page may be nested. Leave blank for no limit.', - 'max_items_instructions' => 'Set a maximum number of selectable items.', - 'navigation_configure_blueprint_instructions' => 'Choose from existing Blueprints or create a new one.', - 'navigation_configure_collections_instructions' => 'Enable linking to entries in these collections.', - 'navigation_configure_handle_instructions' => 'Used to reference this navigation on the frontend. It\'s non-trivial to change later.', - 'navigation_configure_intro' => 'Navigations are multi-level lists of links that can be used to build navbars, footers, sitemaps, and other forms of frontend navigation.', - 'navigation_configure_select_across_sites' => 'Allow selecting entries from other sites.', - 'navigation_configure_settings_intro' => 'Enable linking to collections, set a max depth, and other behaviors.', - 'navigation_configure_title_instructions' => 'We recommend a name that matches where it will be used, like "Main Nav" or "Footer Nav".', - 'navigation_documentation_instructions' => 'Learn more about building, configuring, and rendering Navigations.', - 'navigation_link_to_entry_instructions' => 'Add a link to an entry. Enable linking to additional collections in the config area.', - 'navigation_link_to_url_instructions' => 'Add a link to any internal or external URL. Enable linking to entries in the config area.', - 'outpost_error_422' => 'Error communicating with statamic.com.', - 'outpost_error_429' => 'Too many requests to statamic.com.', - 'outpost_issue_try_later' => 'There was an issue communicating with statamic.com. Please try again later.', - 'outpost_license_key_error' => 'Statamic was unable to decrypt the provided license key file. Please re-download from statamic.com.', - 'password_protect_enter_password' => 'Enter password to unlock', - 'password_protect_incorrect_password' => 'Incorrect password.', - 'password_protect_token_invalid' => 'Invalid or expired token.', - 'password_protect_token_missing' => 'Secure token is missing. You must arrive at this screen from the original, protected URL.', - 'phpinfo_utility_description' => 'Check PHP configuration settings and installed modules.', - 'preference_favorites_instructions' => 'Shortcuts that will be shown when opening the global search bar. You may alternatively visit the page and use the pin icon at the top to add it to this list.', - 'preference_locale_instructions' => 'The preferred language for the control panel.', - 'preference_start_page_instructions' => 'The page to be shown when logging into the control panel.', - 'publish_actions_create_revision' => 'A revision will be created based off the working copy. The current revision will not change.', - 'publish_actions_current_becomes_draft_because_scheduled' => 'Since the current revision is published and you\'ve selected a date in the future, once you submit, the revision will act like a draft until the selected date.', - 'publish_actions_publish' => 'Changes to the working copy will be applied to the entry and it will be published immediately.', - 'publish_actions_schedule' => 'Changes to the working copy will be applied to the entry and it will appear published on the selected date.', 'publish_actions_schedule_revision' => 'A revision will be created based off the working copy and published on the selected date.', - 'publish_actions_unpublish' => 'The current revision will be unpublished.', - 'reset_password_notification_body' => 'You are receiving this email because we received a password reset request for your account.', - 'reset_password_notification_no_action' => 'If you did not request a password reset, no further action is required.', - 'reset_password_notification_subject' => 'Reset Password Notification', - 'role_change_handle_warning' => 'Changing the handle will not update references to it in users and groups.', - 'role_handle_instructions' => 'Handles are used to reference this role on the frontend. Cannot be easily changed.', - 'role_intro' => 'Roles are groups of access and action permissions that can be assigned to users and user groups.', - 'role_title_instructions' => 'Usually a singular noun, like Editor or Admin.', - 'search_utility_description' => 'Manage and view important information about Statamic\'s search indexes.', - 'session_expiry_enter_password' => 'Enter your password to continue where you left off', - 'session_expiry_logged_out_for_inactivity' => 'You have been logged out because you\'ve been inactive for a while.', - 'session_expiry_logging_out_in_seconds' => 'You have been inactive for a while and will be logged out in :seconds seconds. Click to extend your session.', - 'session_expiry_new_window' => 'Opens in a new window. Come back once you\'ve logged in.', - 'show_slugs_instructions' => 'Whether to display slugs in the tree view.', - 'site_configure_attributes_instructions' => 'Add arbitrary attributes to your site\'s config which can be accessed in your templates. [Learn more](https://statamic.dev/multi-site#additional-attributes).', - 'site_configure_handle_instructions' => 'A unique reference to this site. Non-trivial to change later.', - 'site_configure_lang_instructions' => 'Learn about [Languages](https://statamic.dev/multi-site#language).', - 'site_configure_locale_instructions' => 'Learn about [Locales](https://statamic.dev/multi-site#locale).', - 'site_configure_name_instructions' => 'User-facing name shown throughout the control panel.', - 'site_configure_url_instructions' => 'Learn about [Site URLs](https://statamic.dev/multi-site#url).', - 'status_expired_with_date' => 'Expired on :date', - 'status_published_with_date' => 'Published on :date', - 'status_scheduled_with_date' => 'Scheduled to be published on :date', - 'taxonomies_blueprints_instructions' => 'Terms in this taxonomy may use any of these blueprints.', - 'taxonomies_collections_instructions' => 'The collections that use this taxonomy.', - 'taxonomies_preview_target_refresh_instructions' => 'Automatically refresh the preview while editing. Disabling this will use postMessage.', - 'taxonomies_preview_targets_instructions' => 'The URLs to be viewable within Live Preview. Learn more in the [documentation](https://statamic.dev/live-preview#preview-targets).', - 'taxonomy_configure_handle_instructions' => 'Used to reference this taxonomy on the frontend. It\'s non-trivial to change later.', - 'taxonomy_configure_intro' => 'A taxonomy is a system of classifying data around a set of unique characteristics, such as categories, tags, or colors.', - 'taxonomy_configure_layout_instructions' => 'Set this taxonomy\'s default layout. Terms can override this setting with a `layout` field.', - 'taxonomy_configure_template_instructions' => 'Set this taxonomy\'s default template.', - 'taxonomy_configure_term_template_instructions' => 'Set this taxonomy\'s default template. Terms can override this setting with a `template` field.', - 'taxonomy_configure_title_instructions' => 'We recommend using a plural noun, like "Categories" or "Tags".', - 'taxonomy_next_steps_blueprints_description' => 'Manage the blueprints and fields available for this taxonomy.', - 'taxonomy_next_steps_configure_description' => 'Configure names, associate collections, define blueprints, and more.', - 'taxonomy_next_steps_create_term_description' => 'Create the first term or stub out a handful of placeholder terms, it\'s up to you.', - 'try_again_in_seconds' => '{0,1}Try again now.|Try again in :count seconds.', - 'units.B' => ':count B', - 'units.GB' => ':count GB', - 'units.KB' => ':count KB', - 'units.MB' => ':count MB', - 'units.ms' => ':countms', - 'units.s' => ':counts', - 'updater_require_version_command' => 'To require a specific version, run the following command', - 'updater_update_to_latest_command' => 'To update to the latest version, run the following command', - 'uploader_append_timestamp' => 'Append timestamp', - 'uploader_choose_new_filename' => 'Choose new filename', - 'uploader_discard_use_existing' => 'Discard and use existing file', - 'uploader_overwrite_existing' => 'Overwrite existing file', - 'user_activation_email_not_sent_error' => 'The activation email couldn\'t be sent. Please check your email configuration and try again.', - 'user_groups_intro' => 'User groups allow you to organize users and apply permission-based roles in aggregate.', - 'user_groups_role_instructions' => 'Assign roles to give users in this group all of their corresponding permissions.', - 'user_groups_title_instructions' => 'Usually a plural noun, like Editors or Photographers', - 'user_wizard_account_created' => 'The user account has been created.', - 'user_wizard_intro' => 'Users can be assigned to roles that customize their permissions, access, and abilities throughout the Control Panel.', - 'user_wizard_invitation_body' => 'Activate your new Statamic account on :site to begin managing this website. For your security, the link below expires after :expiry hour. After that, please contact the site administrator for a new password.|Activate your new Statamic account on :site to begin managing this website. For your security, the link below expires after :expiry hours. After that, please contact the site administrator for a new password.', - 'user_wizard_invitation_intro' => 'Send a welcome email with account activation details to the new user.', - 'user_wizard_invitation_share' => 'Copy these credentials and share them with :email via your preferred method.', - 'user_wizard_invitation_share_before' => 'After creating a user, details will be provided to share with :email via your preferred method.', - 'user_wizard_invitation_subject' => 'Activate your new Statamic account on :site', - 'user_wizard_roles_groups_intro' => 'Users can be assigned to roles that customize their permissions, access, and abilities throughout the Control Panel.', - 'user_wizard_super_admin_instructions' => 'Super admins have complete control and access to everything in the control panel. Grant this role wisely.', - 'view_more_count' => 'View :count more', - 'width_x_height' => ':width x :height', -]; From f8a1ae8e19c686c56b9c3c54f1b1d0b646cc8891 Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 15:14:47 -0700 Subject: [PATCH 23/42] =?UTF-8?q?it=E2=80=99s=20in=20the=20store=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Revisions/RevisionRepository.php | 24 ------------------------ src/Stache/Stores/RevisionsStore.php | 9 ++++++++- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/Revisions/RevisionRepository.php b/src/Revisions/RevisionRepository.php index 0ed20994a0a..f91282eef83 100644 --- a/src/Revisions/RevisionRepository.php +++ b/src/Revisions/RevisionRepository.php @@ -2,13 +2,9 @@ namespace Statamic\Revisions; -use Carbon\Carbon; -use Illuminate\Support\Arr; use Statamic\Contracts\Revisions\Revision as RevisionContract; use Statamic\Contracts\Revisions\RevisionQueryBuilder; use Statamic\Contracts\Revisions\RevisionRepository as Contract; -use Statamic\Facades\File; -use Statamic\Facades\YAML; use Statamic\Stache\Stache; use Statamic\Support\Str; @@ -62,26 +58,6 @@ public function delete(RevisionContract $revision) $this->store->delete($revision); } - protected function makeRevisionFromFile($key, $path) - { - $yaml = YAML::parse(File::get($path)); - - $revision = (new Revision) - ->key($key) - ->action($yaml['action'] ?? false) - ->id($date = $yaml['date']) - ->date(Carbon::createFromTimestamp($date, config('app.timezone'))) - ->user($yaml['user'] ?? false) - ->message($yaml['message'] ?? false) - ->attributes($yaml['attributes']); - - if (! is_null($timestamp = Arr::get($yaml, 'publish_at'))) { - $revision->publishAt(Carbon::createFromTimestamp($timestamp)); - } - - return $revision; - } - public function query() { return app(RevisionQueryBuilder::class); diff --git a/src/Stache/Stores/RevisionsStore.php b/src/Stache/Stores/RevisionsStore.php index 7e06deb684d..8df0dcc930c 100644 --- a/src/Stache/Stores/RevisionsStore.php +++ b/src/Stache/Stores/RevisionsStore.php @@ -2,6 +2,7 @@ namespace Statamic\Stache\Stores; +use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Statamic\Facades\File; use Statamic\Facades\Path; @@ -25,7 +26,7 @@ public function makeItemFromFile($path, $contents) $yaml['action'] = 'working'; } - return Revision::make() + $revision = Revision::make() ->initialPath($path) ->key($key) ->action($yaml['action'] ?? false) @@ -33,5 +34,11 @@ public function makeItemFromFile($path, $contents) ->user($yaml['user'] ?? false) ->message($yaml['message'] ?? null) ->attributes($yaml['attributes'] ?? []); + + if (! is_null($timestamp = Arr::get($yaml, 'publish_at'))) { + $revision->publishAt(Carbon::createFromTimestamp($timestamp)); + } + + return $revision; } } From 3cb8951cddb9903aafae535d663a51cb4b658860 Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 15:18:12 -0700 Subject: [PATCH 24/42] fix merge --- src/Contracts/Revisions/Revision.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Contracts/Revisions/Revision.php b/src/Contracts/Revisions/Revision.php index b2076c0a6c8..4e34eaa9db0 100644 --- a/src/Contracts/Revisions/Revision.php +++ b/src/Contracts/Revisions/Revision.php @@ -8,7 +8,7 @@ interface Revision { public function entry(): Entry; - public function id($id = null); + public function id(); public function message($message = null); From cd4288dd0fe83bc6d05580b47d61c8ce4804d2bf Mon Sep 17 00:00:00 2001 From: Erin Dalzell Date: Tue, 17 Mar 2026 15:19:58 -0700 Subject: [PATCH 25/42] Update Revision.php --- src/Revisions/Revision.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index 34b7a72182b..5f5dbe81cf0 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -22,22 +22,13 @@ class Revision implements Arrayable, ContainsQueryableValues, Contract { use ExistsAsFile, FluentlyGetsAndSets, TracksQueriedColumns, TracksQueriedRelations; - protected $id; - protected $key; - protected $date; - protected $user; - protected $userId; - protected $message; - protected $publishAt; - protected $action = 'revision'; - protected $attributes = []; public function entry(): Entry From e08459a2bbfb21087ef8ae42ff9bd600739fec4d Mon Sep 17 00:00:00 2001 From: Erin Dalzell Date: Tue, 17 Mar 2026 15:20:58 -0700 Subject: [PATCH 26/42] Update Revision.php --- src/Revisions/Revision.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index 5f5dbe81cf0..1ff6c5eef05 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -36,7 +36,7 @@ public function entry(): Entry return EntryFacade::find($this->attribute('id')); } - public function id($id = null) + public function id() { return $this->key.'/'.($this->isWorkingCopy() ? 'working' : $this->date()->timestamp); } From 766ce1c68ae96e2a139bba749c08ec1ab33b4ce2 Mon Sep 17 00:00:00 2001 From: Erin Dalzell Date: Tue, 17 Mar 2026 15:21:36 -0700 Subject: [PATCH 27/42] Update RevisionRepository.php --- src/Revisions/RevisionRepository.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Revisions/RevisionRepository.php b/src/Revisions/RevisionRepository.php index f91282eef83..1c4d6f93c5c 100644 --- a/src/Revisions/RevisionRepository.php +++ b/src/Revisions/RevisionRepository.php @@ -11,7 +11,6 @@ class RevisionRepository implements Contract { protected $stache; - protected $store; public function __construct(Stache $stache) From 98b8ff818dcb0920b3e604bfe30d23f442fb3339 Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 15:43:30 -0700 Subject: [PATCH 28/42] fix tests --- tests/Feature/Revisions/RevisableTest.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/Feature/Revisions/RevisableTest.php b/tests/Feature/Revisions/RevisableTest.php index 9d46d91cf11..29038eab32c 100644 --- a/tests/Feature/Revisions/RevisableTest.php +++ b/tests/Feature/Revisions/RevisableTest.php @@ -19,10 +19,7 @@ public function setUp(): void { parent::setUp(); - config(['statamic.revisions' => [ - 'enabled' => true, - 'path' => __DIR__.'/__fixtures__', - ]]); + config(['statamic.revisions' => ['enabled' => true]]); $this->revisable = new class { @@ -47,12 +44,6 @@ public function makeFromRevision($revision) }; } - #[Test] - public function has_revisions() - { - $this->assertTrue($this->revisable->hasRevisions()); - } - #[Test] public function sets_publish_at_from_options() { From 3a6e0351af4d7365195a76ae01e62ce55d798e16 Mon Sep 17 00:00:00 2001 From: edalzell Date: Tue, 17 Mar 2026 15:59:32 -0700 Subject: [PATCH 29/42] maybe this? --- .../js/components/entries/PublishActions.vue | 90 +++---------------- 1 file changed, 14 insertions(+), 76 deletions(-) diff --git a/resources/js/components/entries/PublishActions.vue b/resources/js/components/entries/PublishActions.vue index 02f135b5c73..08b89b3e84f 100644 --- a/resources/js/components/entries/PublishActions.vue +++ b/resources/js/components/entries/PublishActions.vue @@ -1,76 +1,4 @@