Skip to content

Commit 2fdfd3a

Browse files
authored
Merge pull request #13906 from craftcms/feature/cms-1216-nested-element-card-views
Nested element card views
2 parents 043cd79 + 893406f commit 2fdfd3a

File tree

21 files changed

+923
-424
lines changed

21 files changed

+923
-424
lines changed

CHANGELOG-WIP.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- Entry edit pages now include quick links to other sections’ index sources.
1010
- Asset edit pages now include quick links to other volumes’ index sources.
1111
- Entry conditions can now have a “Matrix field” rule. ([#13794](https://github.com/craftcms/cms/discussions/13794))
12-
- User addresses are now displayed within an embedded element index.
12+
- User addresses can now be displayed as an embedded element index.
1313
- Selected elements within relational fields now include a context menu with “View in a new tab”, “Edit”, and “Remove” options.
1414
- Selected elements within relational fields now include a dedicated drag handle.
1515
- Selected assets within Assets fields no longer open the file preview modal when their thumbnail is clicked on. The “Preview file” quick action, or the <kbd>Shift</kbd> + <kbd>Spacebar</kbd> keyboard shortcut, can be used instead.
@@ -31,10 +31,10 @@
3131
- Most custom fields can now be included multiple times within the same field layout. ([#8497](https://github.com/craftcms/cms/discussions/8497))
3232
- Entry types are now managed independently of sections.
3333
- Entry types are no longer required to have a Title Format, if the Title field isn’t shown.
34-
- Added the “Addresses” field type. ([#11438](https://github.com/craftcms/cms/discussions/11438))
34+
- Added the “Addresses” field type. ([#11438](https://github.com/craftcms/cms/discussions/11438))
3535
- 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.
3636
- Matrix fields now have “Entry URI Format” and “Template” settings for each site.
37-
- Matrix fields now have a “View Mode” setting, which can be used to have nested entries display within an element index, rather than as inline blocks.
37+
- 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.
3838
- The address field layout is now accessed via **Settings****Addresses**.
3939
- 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))
4040
- Added support for defining custom locale aliases, via a new `localeAliases` config setting. ([#12705](https://github.com/craftcms/cms/pull/12705))
@@ -249,6 +249,7 @@
249249
- `craft\db\Connection::getSupportsMb4()` is now dynamic for MySQL installs, based on whether the `elements_sites` table has an `mb4` charset.
250250
- `craft\elemens\db\ElementQueryInterface::collect()` now has an `ElementCollection` return type, rather than `Collection`.
251251
- `craft\elements\Entry::getSection()` can now return `null`, for nested entries.
252+
- `craft\elements\User::getAddresses()` now returns a collection.
252253
- `craft\enums\LicenseKeyStatus` is now an enum.
253254
- `craft\fields\BaseOptionsField::$multi` and `$optgroups` properties are now static.
254255
- `craft\fields\Matrix::$propagationMethod` now has a type of `craft\enums\PropagationMethod`.

src/controllers/NestedElementsController.php

+87-25
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use craft\helpers\Db;
1818
use craft\web\Controller;
1919
use yii\web\BadRequestHttpException;
20+
use yii\web\ForbiddenHttpException;
2021
use yii\web\Response;
2122

2223
/**
@@ -27,6 +28,9 @@
2728
*/
2829
class NestedElementsController extends Controller
2930
{
31+
private ElementInterface $owner;
32+
private ElementQueryInterface|ElementCollection $nestedElements;
33+
3034
/**
3135
* @inheritdoc
3236
*/
@@ -37,51 +41,52 @@ public function beforeAction($action): bool
3741
}
3842

3943
$this->requireCpRequest();
40-
return true;
41-
}
4244

43-
/**
44-
* Moves the given elements to a new starting offset
45-
*
46-
* @return Response
47-
*/
48-
public function actionReorder(): Response
49-
{
45+
// Get the owner element
5046
/** @var ElementInterface|string $ownerElementType */
5147
$ownerElementType = $this->request->getRequiredBodyParam('ownerElementType');
5248
$ownerId = $this->request->getRequiredBodyParam('ownerId');
5349
$ownerSiteId = $this->request->getRequiredBodyParam('ownerSiteId');
54-
$attribute = $this->request->getRequiredBodyParam('attribute');
55-
$ids = array_map(fn($id) => (int)$id, $this->request->getRequiredBodyParam('elementIds'));
56-
$offset = $this->request->getRequiredBodyParam('offset');
57-
58-
$elementsService = Craft::$app->getElements();
59-
$owner = $elementsService->getElementById($ownerId, $ownerElementType, $ownerSiteId);
60-
50+
$owner = Craft::$app->getElements()->getElementById($ownerId, $ownerElementType, $ownerSiteId);
6151
if (!$owner) {
6252
throw new BadRequestHttpException('Invalid owner params');
6353
}
54+
$this->owner = $owner;
6455

56+
// Make sure they're authorized to manage it
6557
$authorizedOwnerId = $owner->id;
6658
if ($owner->isProvisionalDraft) {
6759
/** @var ElementInterface|DraftBehavior $owner */
6860
if ($owner->creatorId === Craft::$app->getUser()->getIdentity()?->id) {
6961
$authorizedOwnerId = $owner->getCanonicalId();
7062
}
7163
}
72-
$this->requireAuthorization(sprintf('editNestedElements::%s::%s', $authorizedOwnerId, $attribute));
64+
$attribute = $this->request->getRequiredBodyParam('attribute');
65+
$this->requireAuthorization(sprintf('manageNestedElements::%s::%s', $authorizedOwnerId, $attribute));
7366

74-
// Get the current sort orders, so we know what needs to change
75-
/** @var ElementQueryInterface|ElementCollection $nestedElements */
76-
$nestedElements = $owner->$attribute;
67+
// Set the nested elements for the action
68+
$this->nestedElements = $this->owner->$attribute;
7769

78-
if ($nestedElements instanceof ElementQueryInterface) {
79-
$oldSortOrders = (clone $nestedElements)
70+
return true;
71+
}
72+
73+
/**
74+
* Moves the given elements to a new starting offset
75+
*
76+
* @return Response
77+
*/
78+
public function actionReorder(): Response
79+
{
80+
$ids = array_map(fn($id) => (int)$id, $this->request->getRequiredBodyParam('elementIds'));
81+
$offset = $this->request->getRequiredBodyParam('offset');
82+
83+
if ($this->nestedElements instanceof ElementQueryInterface) {
84+
$oldSortOrders = (clone $this->nestedElements)
8085
->asArray()
8186
->select(['id', 'sortOrder'])
8287
->pairs();
8388
} else {
84-
$oldSortOrders = $nestedElements
89+
$oldSortOrders = $this->nestedElements
8590
->keyBy(fn(NestedElementInterface $element) => $element->id)
8691
->map(fn(NestedElementInterface $element) => $element->getSortOrder())
8792
->all();
@@ -98,16 +103,73 @@ public function actionReorder(): Response
98103
Db::update(Table::ELEMENTS_OWNERS, [
99104
'sortOrder' => $sortOrder,
100105
], [
101-
'ownerId' => $owner->id,
106+
'ownerId' => $this->owner->id,
102107
'elementId' => $id,
103108
]);
104109
}
105110
}
106111

107-
$elementsService->invalidateCachesForElement($owner);
112+
Craft::$app->getElements()->invalidateCachesForElement($this->owner);
108113

109114
return $this->asSuccess(Craft::t('app', 'New {total, plural, =1{position} other{positions}} saved.', [
110115
'total' => count($ids),
111116
]));
112117
}
118+
119+
/**
120+
* Deletes a given element.
121+
*
122+
* @return Response
123+
*/
124+
public function actionDelete(): Response
125+
{
126+
$elementId = (int)$this->request->getRequiredBodyParam('elementId');
127+
128+
if ($this->nestedElements instanceof ElementQueryInterface) {
129+
$element = $this->nestedElements
130+
->id($elementId)
131+
->status(null)
132+
->drafts(null)
133+
->provisionalDrafts(null)
134+
->one();
135+
} else {
136+
$element = $this->nestedElements->first(
137+
fn(ElementInterface $element) => (
138+
$element->id === $elementId ||
139+
$element->getCanonicalId() === $elementId
140+
)
141+
);
142+
}
143+
144+
if (!$element) {
145+
throw new BadRequestHttpException('Invalid elementId param');
146+
}
147+
148+
$elementsService = Craft::$app->getElements();
149+
150+
if (!$elementsService->canDelete($element)) {
151+
throw new ForbiddenHttpException('User not authorized to delete this element.');
152+
}
153+
154+
// If the element primarily belongs to a different element, just delete the ownership
155+
if ($element->getPrimaryOwnerId() !== $this->owner->id) {
156+
Db::delete(Table::ELEMENTS_OWNERS, [
157+
'elementId' => $element->id,
158+
'ownerId' => $this->owner->id,
159+
]);
160+
$success = true;
161+
} else {
162+
$success = $elementsService->deleteElement($element);
163+
}
164+
165+
if (!$success) {
166+
return $this->asFailure(Craft::t('app', 'Couldn’t delete {type}.', [
167+
'type' => $element::lowerDisplayName(),
168+
]));
169+
}
170+
171+
return $this->asSuccess(Craft::t('app', '{type} deleted.', [
172+
'type' => $element::displayName(),
173+
]));
174+
}
113175
}

0 commit comments

Comments
 (0)