Skip to content

Commit 6c21e4b

Browse files
authored
Merge pull request #17999 from craftcms/feature/revisions-drafts
[6.x] Revisions, Drafts & ElementSources services
2 parents 9ec9e6b + 3dcfb78 commit 6c21e4b

File tree

75 files changed

+2541
-1724
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2541
-1724
lines changed

CHANGELOG-WIP.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ Craft's Mutex classes have been deprecated. [Laravel's atomic locking](https://l
127127
- `craft\services\Dashboard::EVENT_BEFORE_DELETE_WIDGET` => `CraftCms\Cms\Dashboard\Events\WidgetDeleting`
128128
- `craft\services\Dashboard::EVENT_AFTER_DELETE_WIDGET` => `CraftCms\Cms\Dashboard\Events\WidgetDeleted`
129129

130+
## Drafts
131+
132+
- Deprecated `craft\services\Drafts`. `CraftCms\Cms\Element\Drafts` should be used instead.
133+
- Deprecated `craft\events\DraftEvent`. One of the events extending `CraftCms\Cms\Element\Events\DraftEvent` should be used instead.
134+
- Deprecated `craft\behaviors\DraftBehavior`. `CraftCms\Cms\Element\Concerns\Draftable` should be used instead.
135+
136+
## ElementSources
137+
138+
- Deprecated `craft\services\ElementSources`. `CraftCms\Cms\Element\ElementSources` should be used instead.
139+
- Deprecated `craft\events\DefineSourceSortOptionsEvent`. `CraftCms\Cms\Element\Events\DefineSourceSortOptions` should be used instead.
140+
- Deprecated `craft\events\DefineSourceTableAttributesEvent`. `CraftCms\Cms\Element\Events\DefineSourceTableAttributes` should be used instead.
141+
130142
## Entries & Entry Types
131143

132144
- Deprecated `craft\services\Entries`. `CraftCms\Cms\Entry\Entries` and `CraftCms\Cms\Entry\EntryTypes` should be used instead.
@@ -276,6 +288,12 @@ Moved the following controllers:
276288
- Removed `craft\models\ReadOnlyProjectConfigData` in favor of `CraftCms\Cms\ProjectConfig\Data\ReadOnlyProjectConfigData`
277289
- Deprecated `craft\helpers\ProjectConfig`. `CraftCms\Cms\ProjectConfig\ProjectConfigHelper` should be used instead.
278290

291+
## Revisions
292+
293+
- Deprecated `craft\services\Revisions`. `CraftCms\Cms\Element\Revisions` should be used instead.
294+
- Deprecated `craft\events\RevisionEvent`. One of the events extending `CraftCms\Cms\Element\Events\RevisionEvent` should be used instead.
295+
- Deprecated `craft\behaviors\RevisionBehavior`. `CraftCms\Cms\Element\Concerns\Revisionable` should be used instead.
296+
279297
## Routes
280298

281299
- Deprecated `craft\services\Routes`. `CraftCms\Cms\Route\Routes` should be used instead.

resources/templates/entries/index.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% extends "_layouts/elementindex" %}
22

3-
{% if page is defined and not craft.app.elementSources.pageExists("craft\\elements\\Entry", page) %}
4-
{% set firstPage = craft.app.elementSources.getFirstPage("craft\\elements\\Entry") %}
3+
{% if page is defined and not craft.elementSources.pageExists("craft\\elements\\Entry", page) %}
4+
{% set firstPage = craft.elementSources.getFirstPage("craft\\elements\\Entry") %}
55
{% if firstPage or (not firstPage and page != 'entries') %}
66
{% redirect 'content/' ~ (firstPage ? firstPage|kebab : 'entries') %}
77
{% endif %}

src/Element/Concerns/Draftable.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Element\Concerns;
6+
7+
use craft\elements\User as UserElement;
8+
use CraftCms\Cms\Database\Table;
9+
use CraftCms\Cms\Element\Events\AuthorizeCreateDrafts;
10+
use CraftCms\Cms\User\Models\User;
11+
use Illuminate\Support\Facades\DB;
12+
use Illuminate\Support\Facades\Event;
13+
14+
/** @phpstan-ignore trait.unused */
15+
trait Draftable
16+
{
17+
/**
18+
* @var int|null The ID of the draft’s row in the `drafts` table
19+
*/
20+
public ?int $draftId = null;
21+
22+
/**
23+
* @var bool Whether this is a provisional draft.
24+
*/
25+
public bool $isProvisionalDraft = false;
26+
27+
/**
28+
* @var bool Whether provisional changes have been loaded onto this element.
29+
*/
30+
public bool $hasProvisionalChanges = false;
31+
32+
/**
33+
* @var int|null The creator’s ID
34+
*/
35+
public ?int $draftCreatorId = null;
36+
37+
/**
38+
* @var string|null The draft name
39+
*/
40+
public ?string $draftName = null;
41+
42+
/**
43+
* @var string|null The draft notes
44+
*/
45+
public ?string $draftNotes = null;
46+
47+
/**
48+
* @var bool Whether to track changes in this draft
49+
*/
50+
public bool $trackDraftChanges = true;
51+
52+
/**
53+
* @var bool Whether the draft should be marked as saved (if unpublished).
54+
*/
55+
public bool $markDraftAsSaved = true;
56+
57+
/**
58+
* @var UserElement|null|false The creator
59+
*/
60+
private UserElement|false|null $draftCreator = null;
61+
62+
/**
63+
* Returns the draft’s creator.
64+
*/
65+
public function getDraftCreator(): ?UserElement
66+
{
67+
if (! isset($this->draftCreator)) {
68+
if (! $this->draftCreatorId) {
69+
return null;
70+
}
71+
72+
/** @var UserElement|null $creator */
73+
$creator = UserElement::find()
74+
->id($this->draftCreatorId)
75+
->status(null)
76+
->one();
77+
78+
$this->draftCreator = $creator ?? false;
79+
}
80+
81+
return $this->draftCreator ?: null;
82+
}
83+
84+
/**
85+
* Sets the draft's creator.
86+
*/
87+
public function setDraftCreator(?UserElement $creator = null): void
88+
{
89+
$this->draftCreator = $creator ?? false;
90+
}
91+
92+
public function getDraftName(): string
93+
{
94+
return $this->draftName;
95+
}
96+
97+
public function handleDraftSave(): void
98+
{
99+
if (! $this->getIsDraft()) {
100+
return;
101+
}
102+
103+
DB::table(Table::DRAFTS)
104+
->where('id', $this->draftId)
105+
->update([
106+
'provisional' => $this->isProvisionalDraft,
107+
'name' => $this->draftName,
108+
'notes' => $this->draftNotes,
109+
'dateLastMerged' => $this->dateLastMerged,
110+
'saved' => $this->markDraftAsSaved,
111+
]);
112+
}
113+
114+
public function handleDraftDelete(): void
115+
{
116+
if (! $this->getIsDraft()) {
117+
return;
118+
}
119+
120+
if (! $this->hardDelete) {
121+
return;
122+
}
123+
124+
DB::table(Table::DRAFTS)->delete($this->draftId);
125+
}
126+
127+
/**
128+
* {@inheritdoc}
129+
*/
130+
public function canCreateDrafts(User $user): bool
131+
{
132+
if (Event::hasListeners(AuthorizeCreateDrafts::class)) {
133+
Event::dispatch($event = new AuthorizeCreateDrafts($this, $user));
134+
135+
return $event->authorized;
136+
}
137+
138+
return false;
139+
}
140+
141+
/**
142+
* {@inheritdoc}
143+
*/
144+
public function canDuplicateAsDraft(UserElement $user): bool
145+
{
146+
// if anything, this will be more lenient than canDuplicate()
147+
return \Craft::$app->getElements()->canDuplicate($this, $user);
148+
}
149+
150+
/**
151+
* {@inheritdoc}
152+
*/
153+
public function getIsDraft(): bool
154+
{
155+
return ! empty($this->draftId);
156+
}
157+
158+
/**
159+
* {@inheritdoc}
160+
*/
161+
public static function hasDrafts(): bool
162+
{
163+
return false;
164+
}
165+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Element\Concerns;
6+
7+
use craft\base\ElementInterface;
8+
use craft\elements\User;
9+
use craft\helpers\UrlHelper;
10+
use CraftCms\Cms\Database\Table;
11+
use CraftCms\Cms\Support\Facades\Sites;
12+
use Illuminate\Support\Facades\DB;
13+
14+
use function CraftCms\Cms\t;
15+
16+
/** @phpstan-ignore trait.unused */
17+
trait Revisionable
18+
{
19+
/**
20+
* @var int|null The creator’s ID
21+
*/
22+
public ?int $revisionCreatorId = null;
23+
24+
/**
25+
* @var ?int The revision number
26+
*/
27+
public ?int $revisionNum = null;
28+
29+
/**
30+
* @var string|null The revision notes
31+
*/
32+
public ?string $revisionNotes = null;
33+
34+
/**
35+
* @var User|null|false The creator
36+
*/
37+
private User|false|null $revisionCreator = null;
38+
39+
/**
40+
* @see getCurrentRevision()
41+
*/
42+
protected ElementInterface|false|null $currentRevision = null;
43+
44+
/**
45+
* Returns the revision’s creator.
46+
*/
47+
public function getRevisionCreator(): ?User
48+
{
49+
if (! isset($this->revisionCreator)) {
50+
if (! $this->revisionCreatorId) {
51+
return null;
52+
}
53+
54+
/** @var User|null $creator */
55+
$creator = User::find()
56+
->id($this->revisionCreatorId)
57+
->status(null)
58+
->one();
59+
60+
$this->revisionCreator = $creator ?? false;
61+
}
62+
63+
return $this->revisionCreator ?: null;
64+
}
65+
66+
/**
67+
* Sets the revision's creator.
68+
*/
69+
public function setRevisionCreator(?User $creator = null): void
70+
{
71+
$this->revisionCreator = $creator ?? false;
72+
}
73+
74+
public function setRevisionCreatorId(?int $creatorId): void
75+
{
76+
$this->revisionCreatorId = $creatorId;
77+
}
78+
79+
public function setRevisionNotes(?string $notes): void
80+
{
81+
$this->revisionNotes = $notes;
82+
}
83+
84+
public function getRevisionLabel(): string
85+
{
86+
return t('Revision {num}', [
87+
'num' => $this->revisionNum,
88+
]);
89+
}
90+
91+
public function handleRevisionDelete(): void
92+
{
93+
if (! $this->getIsRevision()) {
94+
return;
95+
}
96+
97+
DB::table(Table::REVISIONS)->delete($this->revisionId);
98+
}
99+
100+
/**
101+
* {@inheritdoc}
102+
*/
103+
public function getIsRevision(): bool
104+
{
105+
return ! empty($this->revisionId);
106+
}
107+
108+
/**
109+
* {@inheritdoc}
110+
*/
111+
public function getCpRevisionsUrl(): ?string
112+
{
113+
$cpEditUrl = $this->cpRevisionsUrl();
114+
115+
if (! $cpEditUrl) {
116+
return null;
117+
}
118+
119+
$params = [];
120+
121+
if (Sites::isMultiSite()) {
122+
$params['site'] = $this->getSite()->handle;
123+
}
124+
125+
return UrlHelper::cpUrl($cpEditUrl, $params);
126+
}
127+
128+
/**
129+
* Returns the element’s revisions index URL in the control panel.
130+
*/
131+
protected function cpRevisionsUrl(): ?string
132+
{
133+
return null;
134+
}
135+
136+
/**
137+
* {@inheritdoc}
138+
*/
139+
public function hasRevisions(): bool
140+
{
141+
return false;
142+
}
143+
144+
abstract public function getCanonical(bool $anySite = false): ElementInterface;
145+
146+
/**
147+
* {@inheritdoc}
148+
*/
149+
public function getCurrentRevision(): ?ElementInterface
150+
{
151+
if (! $this->id) {
152+
return null;
153+
}
154+
155+
if (! isset($this->currentRevision)) {
156+
$canonical = $this->getCanonical(true);
157+
$this->currentRevision = static::find()
158+
->siteId($canonical->siteId)
159+
->revisionOf($canonical->id)
160+
->dateCreated($canonical->dateUpdated)
161+
->status(null)
162+
->orderBy(['num' => SORT_DESC])
163+
->one() ?: false;
164+
}
165+
166+
return $this->currentRevision ?: null;
167+
}
168+
}

0 commit comments

Comments
 (0)