Skip to content

Commit a0e98b3

Browse files
committed
Matrix fixes
1 parent bd5cd68 commit a0e98b3

17 files changed

Lines changed: 423 additions & 269 deletions

File tree

CHANGELOG-WIP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
- Matrix fields now manage nested entries, rather than Matrix blocks. During the upgrade, existing Matrix block types will be converted to entry types; their nested fields will be made global; and Matrix blocks will be converted to entries.
7070
- Matrix fields now have “Entry URI Format” and “Template” settings for each site.
7171
- Matrix fields now have a “View Mode” setting, giving admins the choice to display nested entries as cards, inline-editable blocks, or an embedded element index.
72+
- Matrix fields now require the owner element to be saved before they can be edited.
7273
- The Fields and Entry Types index pages now have a search bar. ([#13961](https://github.com/craftcms/cms/discussions/13961), [#14126](https://github.com/craftcms/cms/pull/14126))
7374
- The address field layout is now accessed via **Settings****Addresses**.
7475
- Volumes now have a “Subpath” setting, and can reuse filesystems so long as the subpaths don’t overlap. ([#11044](https://github.com/craftcms/cms/discussions/11044))

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Added live conditional field support to inline-editable Matrix blocks. ([#14223](https://github.com/craftcms/cms/pull/14223))
66
- Inline-editable Matrix blocks have been redesigned to be visually lighter. ([#14187](https://github.com/craftcms/cms/pull/14187))
77
- Matrix fields set to the inline-editable blocks view mode no longer show inline entry-creation buttons unless there’s a single entry type. ([#14187](https://github.com/craftcms/cms/pull/14187))
8+
- Matrix fields now require the owner element to be saved before they can be edited, even when the inline-editable blocks view mode is selected.
89
- Improved the accessibility of Matrix fields with the “inline-editable blocks” view mode. ([#14187](https://github.com/craftcms/cms/pull/14187))
910
- Added the “Color” entry type setting. ([#14187](https://github.com/craftcms/cms/pull/14187))
1011
- Entry chips, cards, and blocks are now tinted according to their entry type’s color. ([#14187](https://github.com/craftcms/cms/pull/14187))
@@ -30,6 +31,7 @@
3031
- Fixed a bug where fields’ Name, Handle, and Instructions placeholder values within field layouts were getting set to the current overridden values.
3132
- Fixed a bug where nested Matrix entries could get deleted when editing multiple of them within the same parent Matrix entry.
3233
- Fixed a bug where empty Dropdown fields were getting treated as dirty when unchanged.
34+
- Fixed a bug where Matrix fields in element index or cards view weren’t working properly when nested within an inline-editable Matrix block.
3335

3436
## 5.0.0-alpha.8 - 2024-01-23
3537

src/controllers/ElementsController.php

Lines changed: 116 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use craft\errors\InvalidTypeException;
2020
use craft\errors\UnsupportedSiteException;
2121
use craft\events\DefineElementEditorHtmlEvent;
22+
use craft\events\DraftEvent;
2223
use craft\helpers\ArrayHelper;
2324
use craft\helpers\Component;
2425
use craft\helpers\Cp;
@@ -29,6 +30,7 @@
2930
use craft\i18n\Locale;
3031
use craft\models\ElementActivity;
3132
use craft\models\FieldLayoutForm;
33+
use craft\services\Drafts;
3234
use craft\web\Controller;
3335
use craft\web\CpScreenResponseBehavior;
3436
use craft\web\UrlManager;
@@ -78,7 +80,6 @@ class ElementsController extends Controller
7880
private ?string $_draftName = null;
7981
private ?string $_notes = null;
8082
private string $_fieldsLocation;
81-
private ?array $_dirtyFields = null;
8283
private bool $_provisional;
8384
private bool $_dropProvisional;
8485
private bool $_addAnother;
@@ -115,7 +116,6 @@ public function beforeAction($action): bool
115116
$this->_draftName = $this->_param('draftName');
116117
$this->_notes = $this->_param('notes');
117118
$this->_fieldsLocation = $this->_param('fieldsLocation') ?? 'fields';
118-
$this->_dirtyFields = $this->_param('dirtyFields');
119119
$this->_provisional = (bool)$this->_param('provisional');
120120
$this->_dropProvisional = (bool)$this->_param('dropProvisional');
121121
$this->_addAnother = (bool)$this->_param('addAnother');
@@ -238,7 +238,7 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null):
238238

239239
if ($element === null) {
240240
/** @var Element|DraftBehavior|RevisionBehavior|Response|null $element */
241-
$element = $this->_element($elementId, null, true, $strictSite);
241+
$element = $this->_element($elementId, checkForProvisionalDraft: true, strictSite: $strictSite);
242242

243243
if ($element instanceof Response) {
244244
return $element;
@@ -525,7 +525,7 @@ public function actionRevisions(int $elementId): Response
525525
$this->requireCpRequest();
526526

527527
/** @var Element|DraftBehavior|RevisionBehavior|Response|null $element */
528-
$element = $this->_element($elementId, null, false);
528+
$element = $this->_element($elementId);
529529

530530
if (!$element) {
531531
throw new BadRequestHttpException('No element was identified by the request.');
@@ -1363,7 +1363,7 @@ public function actionDeleteForSite(): Response
13631363
$this->requirePostRequest();
13641364

13651365
/** @var Element|null $element */
1366-
$element = $this->_element(provisional: true);
1366+
$element = $this->_element(checkForProvisionalDraft: true);
13671367

13681368
if (!$element || $element->getIsRevision()) {
13691369
throw new BadRequestHttpException('No element was identified by the request.');
@@ -1441,11 +1441,21 @@ public function actionSaveDraft(): ?Response
14411441
}
14421442
}
14431443

1444-
return Craft::$app->getDb()->transaction(function() use ($element, $user, $elementsService): ?Response {
1444+
// Keep track of all newly-created draft IDs
1445+
$draftElementIds = [];
1446+
$draftsService = Craft::$app->getDrafts();
1447+
$draftsService->on(Drafts::EVENT_AFTER_CREATE_DRAFT, function(DraftEvent $event) use (&$draftElementIds) {
1448+
$draftElementIds[$event->canonical->id] = $event->draft->id;
1449+
});
1450+
1451+
$db = Craft::$app->getDb();
1452+
$transaction = $db->beginTransaction();
1453+
1454+
try {
14451455
// Are we creating the draft here?
14461456
if (!$element->getIsDraft()) {
14471457
/** @var Element|DraftBehavior $element */
1448-
$draft = Craft::$app->getDrafts()->createDraft($element, $user->id, null, null, [], $this->_provisional);
1458+
$draft = $draftsService->createDraft($element, $user->id, null, null, [], $this->_provisional);
14491459
$draft->setCanonical($element);
14501460
$element = $this->element = $draft;
14511461
}
@@ -1464,46 +1474,108 @@ public function actionSaveDraft(): ?Response
14641474
$element->setScenario(Element::SCENARIO_ESSENTIALS);
14651475

14661476
if (!$elementsService->saveElement($element)) {
1477+
$transaction->rollBack();
14671478
return $this->_asFailure($element, Craft::t('app', 'Couldn’t save {type}.', [
14681479
'type' => Craft::t('app', 'draft'),
14691480
]));
14701481
}
14711482

1472-
$elementsService->trackActivity($element, ElementActivity::TYPE_SAVE);
1483+
$transaction->commit();
1484+
} catch (Throwable $e) {
1485+
$transaction->rollBack();
1486+
throw $e;
1487+
}
14731488

1474-
$creator = $element->getCreator();
1489+
$elementsService->trackActivity($element, ElementActivity::TYPE_SAVE);
14751490

1476-
$data = [
1477-
'canonicalId' => $element->getCanonicalId(),
1478-
'elementId' => $element->id,
1479-
'draftId' => $element->draftId,
1480-
'timestamp' => Craft::$app->getFormatter()->asTimestamp($element->dateUpdated, 'short', true),
1481-
'creator' => $creator?->getName(),
1482-
'draftName' => $element->draftName,
1483-
'draftNotes' => $element->draftNotes,
1484-
'modifiedAttributes' => $element->getModifiedAttributes(),
1491+
$creator = $element->getCreator();
1492+
1493+
$data = [
1494+
'canonicalId' => $element->getCanonicalId(),
1495+
'elementId' => $element->id,
1496+
'draftId' => $element->draftId,
1497+
'timestamp' => Craft::$app->getFormatter()->asTimestamp($element->dateUpdated, 'short', true),
1498+
'creator' => $creator?->getName(),
1499+
'draftName' => $element->draftName,
1500+
'draftNotes' => $element->draftNotes,
1501+
'modifiedAttributes' => $element->getModifiedAttributes(),
1502+
'draftElementIds' => $draftElementIds,
1503+
];
1504+
1505+
if ($this->request->getIsCpRequest()) {
1506+
[$docTitle, $title] = $this->_editElementTitles($element);
1507+
$data += $this->_fieldLayoutData($element);
1508+
$data += [
1509+
'docTitle' => $docTitle,
1510+
'title' => $title,
1511+
'previewTargets' => $element->getPreviewTargets(),
1512+
'initialDeltaValues' => Craft::$app->getView()->getInitialDeltaValues(),
1513+
'updatedTimestamp' => $element->dateUpdated->getTimestamp(),
1514+
'canonicalUpdatedTimestamp' => $element->getCanonical()->dateUpdated->getTimestamp(),
14851515
];
1516+
}
14861517

1487-
if ($this->request->getIsCpRequest()) {
1488-
[$docTitle, $title] = $this->_editElementTitles($element);
1489-
$data += $this->_fieldLayoutData($element);
1490-
$data += [
1491-
'docTitle' => $docTitle,
1492-
'title' => $title,
1493-
'previewTargets' => $element->getPreviewTargets(),
1494-
'initialDeltaValues' => Craft::$app->getView()->getInitialDeltaValues(),
1495-
'updatedTimestamp' => $element->dateUpdated->getTimestamp(),
1496-
'canonicalUpdatedTimestamp' => $element->getCanonical()->dateUpdated->getTimestamp(),
1497-
];
1498-
}
1518+
// Make sure the user is authorized to preview the draft
1519+
Craft::$app->getSession()->authorize("previewDraft:$element->draftId");
14991520

1500-
// Make sure the user is authorized to preview the draft
1501-
Craft::$app->getSession()->authorize("previewDraft:$element->draftId");
1521+
return $this->_asSuccess(Craft::t('app', '{type} saved.', [
1522+
'type' => Craft::t('app', 'Draft'),
1523+
]), $element, $data, true);
1524+
}
15021525

1503-
return $this->_asSuccess(Craft::t('app', '{type} saved.', [
1504-
'type' => Craft::t('app', 'Draft'),
1505-
]), $element, $data, true);
1506-
});
1526+
/**
1527+
* Ensures that a provisional draft exists for the element, unless it’s already a draft.
1528+
*
1529+
* @return Response
1530+
* @since 5.0.0
1531+
*/
1532+
public function actionEnsureDraft(): Response
1533+
{
1534+
$this->requirePostRequest();
1535+
1536+
/** @var Element|DraftBehavior|null $element */
1537+
$element = $this->_element(checkForProvisionalDraft: true);
1538+
1539+
if (!$element || $element->getIsRevision()) {
1540+
throw new BadRequestHttpException('No element was identified by the request.');
1541+
}
1542+
1543+
if ($element->getIsDraft()) {
1544+
return $this->asSuccess(data: [
1545+
'elementId' => $element->id,
1546+
]);
1547+
}
1548+
1549+
$elementsService = Craft::$app->getElements();
1550+
$user = static::currentUser();
1551+
1552+
if (!$elementsService->canCreateDrafts($element, $user)) {
1553+
throw new ForbiddenHttpException('User not authorized to create drafts for this element.');
1554+
}
1555+
1556+
$this->element = $element;
1557+
1558+
// Make sure a provisional draft doesn't already exist for this element/user combo
1559+
$provisionalId = $element::find()
1560+
->provisionalDrafts()
1561+
->draftOf($element->id)
1562+
->draftCreator($user->id)
1563+
->site('*')
1564+
->status(null)
1565+
->ids()[0] ?? null;
1566+
1567+
if ($provisionalId) {
1568+
return $this->asSuccess(data: [
1569+
'elementId' => $provisionalId,
1570+
]);
1571+
}
1572+
1573+
/** @var Element|DraftBehavior $element */
1574+
$draft = Craft::$app->getDrafts()->createDraft($element, $user->id, provisional: true);
1575+
1576+
return $this->asSuccess(data: [
1577+
'elementId' => $draft->id,
1578+
]);
15071579
}
15081580

15091581
/**
@@ -1882,14 +1954,18 @@ public function actionRecentActivity(): Response
18821954
*
18831955
* @param int|null $elementId
18841956
* @param string|null $elementUid
1885-
* @param bool|null $provisional
1957+
* @param bool $checkForProvisionalDraft
18861958
* @param bool $strictSite
18871959
* @return ElementInterface|Response|null
18881960
* @throws BadRequestHttpException
18891961
* @throws ForbiddenHttpException
18901962
*/
1891-
private function _element(?int $elementId = null, ?string $elementUid = null, ?bool $provisional = null, bool $strictSite = true): ElementInterface|Response|null
1892-
{
1963+
private function _element(
1964+
?int $elementId = null,
1965+
?string $elementUid = null,
1966+
bool $checkForProvisionalDraft = false,
1967+
bool $strictSite = true,
1968+
): ElementInterface|Response|null {
18931969
$elementId = $elementId ?? $this->_elementId;
18941970
$elementUid = $elementUid ?? $this->_elementUid;
18951971

@@ -1957,7 +2033,7 @@ private function _element(?int $elementId = null, ?string $elementUid = null, ?b
19572033
} elseif ($elementId || $elementUid) {
19582034
if ($elementId) {
19592035
// First check for a provisional draft, if we're open to it
1960-
if ($provisional) {
2036+
if ($checkForProvisionalDraft) {
19612037
$element = $elementType::find()
19622038
->provisionalDrafts()
19632039
->draftOf($elementId)
@@ -2127,11 +2203,6 @@ private function _applyParamsToElement(ElementInterface $element): void
21272203

21282204
// Set the custom field values
21292205
$element->setFieldValuesFromRequest($this->_fieldsLocation);
2130-
2131-
// Mark additional fields as dirty?
2132-
if (!empty($this->_dirtyFields)) {
2133-
$element->setDirtyFields($this->_dirtyFields);
2134-
}
21352206
}
21362207

21372208
/**

src/controllers/MatrixController.php

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
use craft\elements\ElementCollection;
1313
use craft\elements\Entry;
1414
use craft\fields\Matrix;
15+
use craft\helpers\ElementHelper;
1516
use craft\helpers\StringHelper;
1617
use craft\web\Controller;
1718
use yii\web\BadRequestHttpException;
19+
use yii\web\ForbiddenHttpException;
1820
use yii\web\Response;
1921

2022
/**
@@ -63,16 +65,16 @@ public function actionDefaultTableColumnOptions(): Response
6365
}
6466

6567
/**
66-
* Renders a new entry block.
68+
* Creates a new entry and renders its block UI.
6769
*
6870
* @return Response
6971
*/
70-
public function actionRenderBlock(): Response
72+
public function actionCreateEntry(): Response
7173
{
7274
$fieldId = $this->request->getRequiredBodyParam('fieldId');
7375
$entryTypeId = $this->request->getRequiredBodyParam('entryTypeId');
74-
$ownerId = $this->request->getBodyParam('ownerId');
75-
$ownerElementType = $this->request->getBodyParam('ownerElementType');
76+
$ownerId = $this->request->getRequiredBodyParam('ownerId');
77+
$ownerElementType = $this->request->getRequiredBodyParam('ownerElementType');
7678
$siteId = $this->request->getRequiredBodyParam('siteId');
7779
$namespace = $this->request->getRequiredBodyParam('namespace');
7880

@@ -91,10 +93,10 @@ public function actionRenderBlock(): Response
9193
throw new BadRequestHttpException("Invalid site ID: $siteId");
9294
}
9395

94-
if ($ownerId) {
95-
$owner = Craft::$app->getElements()->getElementById($ownerId, $ownerElementType, $siteId);
96-
} else {
97-
$owner = null;
96+
$elementsService = Craft::$app->getElements();
97+
$owner = $elementsService->getElementById($ownerId, $ownerElementType, $siteId);
98+
if (!$owner) {
99+
throw new BadRequestHttpException("Invalid owner ID, element type, or site ID.");
98100
}
99101

100102
$entry = Craft::createObject([
@@ -103,15 +105,27 @@ public function actionRenderBlock(): Response
103105
'uid' => StringHelper::UUID(),
104106
'typeId' => $entryType->id,
105107
'fieldId' => $fieldId,
106-
'owner' => $owner ?? null,
108+
'owner' => $owner,
109+
'slug' => ElementHelper::tempSlug(),
107110
]);
108111

109-
/** @var EntryQuery|ElementCollection|null $value */
110-
$value = $owner?->getFieldValue($field->handle);
112+
$user = static::currentUser();
113+
if (!$elementsService->canSave($entry, $user)) {
114+
throw new ForbiddenHttpException('User not authorized to create this element.');
115+
}
116+
117+
if (!$elementsService->saveElement($entry, false)) {
118+
return $this->asFailure(Craft::t('app', 'Couldn’t create {type}.', [
119+
'type' => Entry::lowerDisplayName(),
120+
]));
121+
}
122+
123+
/** @var EntryQuery|ElementCollection $value */
124+
$value = $owner->getFieldValue($field->handle);
111125

112126
$view = $this->getView();
113127
/** @var Entry[] $entries */
114-
$entries = $value?->all() ?? [];
128+
$entries = $value->all();
115129
$html = $view->namespaceInputs(fn() => $view->renderTemplate('_components/fieldtypes/Matrix/block.twig', [
116130
'name' => $field->handle,
117131
'entryTypes' => $field->getEntryTypesForField($entries, $owner),

0 commit comments

Comments
 (0)