Skip to content

[5.x] Scheduled Revision Publishing #11508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: 5.x
Choose a base branch
from
152 changes: 88 additions & 64 deletions resources/js/components/entries/PublishActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,74 @@

<stack narrow name="publish-options" @closed="$emit('closed')">
<div slot-scope="{ close }" class="bg-white dark:bg-dark-800 h-full flex flex-col">

<div class="bg-gray-200 dark:bg-dark-600 px-6 py-2 border-b border-gray-300 dark:border-dark-900 text-lg font-medium flex items-center justify-between">
{{ __('Publish') }}
<button
type="button"
class="btn-close"
@click="close"
v-html="'&times'" />
</div>

<div class="flex-1 overflow-auto p-6">

<div class="flex h-full items-center justify-center loading" v-if="saving">
<loading-graphic text="" />
</div>

<template v-else>

<select-input
class="mb-6"
v-model="action"
:options="options"
/>

<div v-if="action">

<date-fieldtype
v-if="action == 'schedule'"
class="mb-6"
name="publishTime"
:value="publishTime" />

<textarea-input
class="mb-6 text-sm"
v-model="revisionMessage"
:placeholder="__('Notes about this revision')"
@keydown.enter="submit"
:focus="true" />

<button
class="btn-primary w-full mb-6"
v-text="submitButtonText"
@click="submit"
/>

<div class="text-gray text-xs flex mb-6">
<div class="pt-px w-4 rtl:ml-2 ltr:mr-2">
<svg-icon name="info-circle" class="pt-px" />
</div>
<div class="flex-1" v-text="actionInfoText" />
<publish-container name="revision-publish-form">
<div>
<div class="bg-gray-200 dark:bg-dark-600 px-6 py-2 border-b border-gray-300 dark:border-dark-900 text-lg font-medium flex items-center justify-between">
{{ __('Publish') }}
<button
type="button"
class="btn-close"
@click="close"
v-html="'&times'" />
</div>

<div class="text-gray text-xs flex mb-6 text-red-500" v-if="action === 'schedule'">
<div class="pt-px w-4 rtl:ml-2 ltr:mr-2">
<svg-icon name="info-circle" class="pt-px" />
</div>
<div class="flex-1" v-text="__('messages.publish_actions_current_becomes_draft_because_scheduled')" />
</div>
<div class="flex-1 overflow-auto p-6">

</div>
<div class="flex h-full items-center justify-center loading" v-if="saving">
<loading-graphic text="" />
</div>

</template>
<template v-else>

<select-input
class="mb-6"
v-model="action"
:options="options"
/>

<div v-if="action">

<date-fieldtype
v-if="action == 'publishLater'"
class="mb-6"
:config="config"
handle="publishLaterDateTime"
v-model="publishRevisionAt" />

<textarea-input
class="mb-6 text-sm"
v-model="revisionMessage"
:placeholder="__('Notes about this revision')"
@keydown.enter="submit"
:focus="true" />

<button
class="btn-primary w-full mb-6"
v-text="submitButtonText"
@click="submit"
/>

<div class="text-gray text-xs flex mb-6">
<div class="pt-px w-4 rtl:ml-2 ltr:mr-2">
<svg-icon name="info-circle" class="pt-px" />
</div>
<div class="flex-1" v-text="actionInfoText" />
</div>

<div class="text-gray text-xs flex mb-6 text-red-500" v-if="action === 'schedule'">
<div class="pt-px w-4 rtl:ml-2 ltr:mr-2">
<svg-icon name="info-circle" class="pt-px" />
</div>
<div class="flex-1" v-text="__('messages.publish_actions_current_becomes_draft_because_scheduled')" />
</div>

</div>

</template>

</div>
</div>
</div>
</publish-container>
</div>
</stack>

Expand All @@ -86,6 +90,12 @@ export default {
data() {
return {
action: this.canManagePublishState ? 'publish' : 'revision',
config: {
earliest_date: this.now(),
latest_date: { date: null, time: null},
time_enabled: true
},
publishRevisionAt: this.now(),
revisionMessage: null,
saving: false,
}
Expand All @@ -98,6 +108,7 @@ export default {

if (this.canManagePublishState) {
options.push({ value: 'publish', label: __('Publish Now') });
options.push({ value: 'publishLater', label: __('Publish Later') });

if (this.published) {
options.push({ value: 'unpublish', label: __('Unpublish') });
Expand All @@ -119,6 +130,8 @@ export default {
return __('messages.publish_actions_unpublish');
case 'revision':
return __('messages.publish_actions_create_revision');
case 'publishLater':
return __('messages.publish_actions_schedule_revision');
}
},

Expand All @@ -130,6 +143,13 @@ export default {

methods: {

now() {
return {
date: moment().format('YYYY-MM-DD'),
time: moment().format('HH:mm')
};
},

submit() {
this.saving = true;
this.$emit('saving');
Expand Down Expand Up @@ -184,10 +204,6 @@ export default {
}).catch(e => {});
},

submitSchedule() {
// todo
},

submitUnpublish() {
const payload = { message: this.revisionMessage };

Expand All @@ -205,9 +221,17 @@ export default {
}).catch(e => 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;
Expand Down
1 change: 1 addition & 0 deletions resources/lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
4 changes: 4 additions & 0 deletions src/Contracts/Revisions/Revision.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

namespace Statamic\Contracts\Revisions;

use Statamic\Entries\Entry;

interface Revision
{
public function entry(): Entry;

public function id($id = null);

public function message($message = null);
Expand Down
27 changes: 27 additions & 0 deletions src/Entries/MinuteRevisions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Statamic\Entries;

use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
use Statamic\Facades\Entry as EntryFacade;

class MinuteRevisions
{
public function __construct(private readonly CarbonInterface $minute)
{
}

public function __invoke(): Collection
{
return EntryFacade::all()
->filter(fn (Entry $entry) => $entry->hasRevisions())
->filter(fn (Entry $entry) => $entry->latestRevision()->publishAt())
->filter(
fn (Entry $entry) => $entry
->latestRevision()
->publishAt()
->isSameMinute($this->minute)
);
}
}
11 changes: 8 additions & 3 deletions src/Http/Controllers/CP/Collections/EntryRevisionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,12 +40,16 @@ public function index(Request $request, $collection, $entry)

public function store(Request $request, $collection, $entry)
{
$entry->createRevision([
$data = [
'message' => $request->message,
'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)
Expand Down
37 changes: 37 additions & 0 deletions src/Jobs/HandleRevisionSchedule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Statamic\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Collection;
use Statamic\Entries\MinuteRevisions;
use Statamic\Events\EntryScheduleReached;
use Statamic\Revisions\Revision;

class HandleRevisionSchedule implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;

public function handle()
{
$this
->revisions()
->each(fn (Revision $revision) => EntryScheduleReached::dispatch(
tap($revision->entry()->makeFromRevision($revision))->save()
));
}

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
// 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 MinuteRevisions($minute))();
}
}
6 changes: 5 additions & 1 deletion src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 9 additions & 1 deletion src/Revisions/Revisable.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

trait Revisable
{
public function hasRevisions(): bool
{
return $this->revisions()->isNotEmpty();
}

public function revision(string $reference)
{
return $this->revisions()->get($reference);
Expand Down Expand Up @@ -162,14 +167,17 @@ public function store($options = [])
return $return;
}

public function createRevision($options = [])
public function createRevision($options = []): self
{
$this
->fromWorkingCopy()
->makeRevision()
->user($options['user'] ?? false)
->message($options['message'] ?? false)
->publishAt($options['publish_at'] ?? null)
->save();

return $this;
}

public function revisionsEnabled()
Expand Down
Loading
Loading