Skip to content

Commit ae0e73f

Browse files
authored
Merge pull request #18665 from craftcms/feature/element-actions
[6.x] Element actions & exporters
2 parents 3bee757 + 28f8965 commit ae0e73f

118 files changed

Lines changed: 5554 additions & 3611 deletions

File tree

Some content is hidden

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

CHANGELOG-WIP.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,8 +434,14 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/
434434
- Added `CraftCms\Cms\Element\ElementActivity`, `CraftCms\Cms\Element\Data\ElementActivity`, `CraftCms\Cms\Element\Enums\ElementActivityType`, and `CraftCms\Cms\Support\Facades\ElementActivity`.
435435
- Deprecated `craft\services\Elements::getRecentActivity()`. `CraftCms\Cms\Element\ElementActivity::getRecentActivity()` should be used instead.
436436
- Deprecated `craft\services\Elements::trackActivity()`. `CraftCms\Cms\Element\ElementActivity::trackActivity()` should be used instead.
437+
- Added `CraftCms\Cms\Element\Actions\ElementAction`, `CraftCms\Cms\Element\ElementActions`, `CraftCms\Cms\Element\Contracts\DeleteActionInterface`, `CraftCms\Cms\Element\Contracts\ElementActionInterface`, `CraftCms\Cms\Element\Events\AfterPerformAction`, `CraftCms\Cms\Element\Events\BeforePerformAction`, `CraftCms\Cms\Http\Controllers\Elements\PerformElementActionController`, and `CraftCms\Cms\Support\Facades\ElementActions`.
438+
- Added Laravel-native element action classes under `CraftCms\Cms\Element\Actions`, `CraftCms\Cms\Asset\Actions`, `CraftCms\Cms\Entry\Actions`, and `CraftCms\Cms\User\Actions`.
439+
- Added `CraftCms\Cms\Element\ElementExporters`, `CraftCms\Cms\Element\Contracts\ElementExporterInterface`, `CraftCms\Cms\Element\Exporters\ElementExporter`, `CraftCms\Cms\Http\Controllers\Elements\ExportElementIndexController`, and `CraftCms\Cms\Support\Facades\ElementExporters`.
440+
- Added Laravel-native element exporter classes under `CraftCms\Cms\Element\Exporters`.
437441
- Deprecated `craft\errors\InvalidTypeException`. `CraftCms\Cms\Element\Exceptions\InvalidTypeException` should be used instead.
438442
- Deprecated `craft\errors\UnsupportedSiteException`. `CraftCms\Cms\Element\Exceptions\UnsupportedSiteException` should be used instead.
443+
- Deprecated `craft\base\ElementAction`, `craft\base\ElementActionInterface`, `craft\elements\actions\DeleteActionInterface`, and the legacy `craft\elements\actions\*` classes. The corresponding `CraftCms\Cms\Element\Actions\*`, `CraftCms\Cms\Asset\Actions\*`, `CraftCms\Cms\Entry\Actions\*`, and `CraftCms\Cms\User\Actions\*` classes should be used instead.
444+
- Deprecated `craft\base\ElementExporter`, `craft\base\ElementExporterInterface`, and the legacy `craft\elements\exporters\*` classes. The corresponding `CraftCms\Cms\Element\Exporters\*` classes should be used instead.
439445

440446
### Validation
441447

routes/actions.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
use CraftCms\Cms\Http\Controllers\Dashboard\Widgets\NewUsersController;
2626
use CraftCms\Cms\Http\Controllers\Dashboard\WidgetsController;
2727
use CraftCms\Cms\Http\Controllers\EditionController;
28+
use CraftCms\Cms\Http\Controllers\Elements\ExportElementIndexController;
29+
use CraftCms\Cms\Http\Controllers\Elements\PerformElementActionController;
2830
use CraftCms\Cms\Http\Controllers\Entries\CreateEntryController;
2931
use CraftCms\Cms\Http\Controllers\Entries\MoveEntryToSectionController;
3032
use CraftCms\Cms\Http\Controllers\Entries\StoreEntryController;
@@ -207,6 +209,10 @@
207209
Route::post('app/switch-to-licensed-edition', [EditionController::class, 'switchToLicensedEdition']);
208210
});
209211

212+
// Elements
213+
Route::post('element-indexes/export', ExportElementIndexController::class);
214+
Route::post('element-indexes/perform-action', PerformElementActionController::class);
215+
210216
// Entries
211217
Route::post('entries/create', CreateEntryController::class);
212218
Route::post('entries/save-entry', StoreEntryController::class);

src/Address/Elements/Address.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
use Craft;
1212
use craft\base\NestedElementInterface;
1313
use craft\base\NestedElementTrait;
14-
use craft\elements\actions\Copy;
1514
use craft\elements\conditions\addresses\AddressCondition;
1615
use CraftCms\Cms\Address\Addresses;
1716
use CraftCms\Cms\Address\Models\Address as AddressModel;
1817
use CraftCms\Cms\Address\Validation\AddressRules;
1918
use CraftCms\Cms\Cms;
2019
use CraftCms\Cms\Database\Table;
20+
use CraftCms\Cms\Element\Actions\Copy;
2121
use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface;
2222
use CraftCms\Cms\Element\Element;
2323
use CraftCms\Cms\Element\Queries\AddressQuery;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Asset\Actions;
6+
7+
use CraftCms\Cms\Element\Actions\ElementAction;
8+
use CraftCms\Cms\Support\Facades\HtmlStack;
9+
use RuntimeException;
10+
11+
use function CraftCms\Cms\t;
12+
13+
class CopyReferenceTag extends ElementAction
14+
{
15+
#[\Override]
16+
public function getTriggerLabel(): string
17+
{
18+
return t('Copy reference tag');
19+
}
20+
21+
public function getTriggerHtml(): ?string
22+
{
23+
$refHandle = $this->elementType::refHandle();
24+
if ($refHandle === null) {
25+
throw new RuntimeException("Element type \"$this->elementType\" doesn't have a reference handle.");
26+
}
27+
28+
HtmlStack::jsWithVars(fn ($type, $refHandle) => <<<JS
29+
(() => {
30+
new Craft.ElementActionTrigger({
31+
type: $type,
32+
bulk: false,
33+
activate: (selectedItems, elementIndex) => {
34+
Craft.ui.createCopyTextPrompt({
35+
label: Craft.t('app', 'Copy the reference tag'),
36+
value: '{' + $refHandle + ':' + selectedItems.find('.element').data('id') + '}',
37+
});
38+
},
39+
})
40+
})();
41+
JS, [static::class, $refHandle]);
42+
43+
return null;
44+
}
45+
}

src/Asset/Actions/CopyUrl.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Asset\Actions;
6+
7+
use CraftCms\Cms\Element\Actions\ElementAction;
8+
use CraftCms\Cms\Support\Facades\HtmlStack;
9+
10+
use function CraftCms\Cms\t;
11+
12+
class CopyUrl extends ElementAction
13+
{
14+
#[\Override]
15+
public function getTriggerLabel(): string
16+
{
17+
return t('Copy URL');
18+
}
19+
20+
public function getTriggerHtml(): ?string
21+
{
22+
HtmlStack::jsWithVars(fn ($type) => <<<JS
23+
(() => {
24+
new Craft.ElementActionTrigger({
25+
type: $type,
26+
bulk: false,
27+
validateSelection: (selectedItems, elementIndex) => !!selectedItems.find('.element').data('url'),
28+
activate: (selectedItems, elementIndex) => {
29+
Craft.ui.createCopyTextPrompt({
30+
label: Craft.t('app', 'Copy the URL'),
31+
value: selectedItems.find('.element').data('url'),
32+
});
33+
},
34+
})
35+
})();
36+
JS, [static::class]);
37+
38+
return null;
39+
}
40+
}

src/Asset/Actions/DeleteAssets.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Asset\Actions;
6+
7+
use CraftCms\Cms\Element\Actions\Delete;
8+
use CraftCms\Cms\Support\Facades\HtmlStack;
9+
10+
class DeleteAssets extends Delete
11+
{
12+
#[\Override]
13+
public function getTriggerHtml(): ?string
14+
{
15+
// Only enable for deletable elements, per canDelete()
16+
HtmlStack::jsWithVars(fn ($type) => <<<JS
17+
(() => {
18+
const trigger = new Craft.ElementActionTrigger({
19+
type: $type,
20+
requireId: false,
21+
validateSelection: (selectedItems, elementIndex) => {
22+
for (let i = 0; i < selectedItems.length; i++) {
23+
const element = selectedItems.eq(i).find('.element');
24+
if (Garnish.hasAttr(element, 'data-is-folder')) {
25+
if (selectedItems.length !== 1) {
26+
// only one folder at a time
27+
return false;
28+
}
29+
const sourcePath = element.data('source-path') || [];
30+
if (!sourcePath.length || !sourcePath[sourcePath.length - 1].canDelete) {
31+
return false;
32+
}
33+
} else {
34+
if (!Garnish.hasAttr(element, 'data-deletable')) {
35+
return false;
36+
}
37+
}
38+
}
39+
40+
return true;
41+
},
42+
43+
activate: (selectedItems, elementIndex) => {
44+
const element = selectedItems.find('.element:first');
45+
if (Garnish.hasAttr(element, 'data-is-folder')) {
46+
const sourcePath = element.data('source-path');
47+
elementIndex.deleteFolder(sourcePath[sourcePath.length - 1])
48+
.then(() => {
49+
elementIndex.updateElements();
50+
});
51+
} else {
52+
elementIndex.submitAction(trigger.\$trigger.data('action'), Garnish.getPostData(trigger.\$trigger));
53+
}
54+
},
55+
});
56+
})();
57+
JS, [static::class]);
58+
59+
return null;
60+
}
61+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Asset\Actions;
6+
7+
use CraftCms\Cms\Element\Actions\ElementAction;
8+
use CraftCms\Cms\Support\Facades\HtmlStack;
9+
10+
use function CraftCms\Cms\t;
11+
12+
class DownloadAssetFile extends ElementAction
13+
{
14+
#[\Override]
15+
public function getTriggerLabel(): string
16+
{
17+
return t('Download');
18+
}
19+
20+
public function getTriggerHtml(): ?string
21+
{
22+
HtmlStack::jsWithVars(fn ($type) => <<<JS
23+
(() => {
24+
new Craft.ElementActionTrigger({
25+
type: $type,
26+
activate: (selectedItems, elementIndex) => {
27+
var \$form = Craft.createForm().appendTo(Garnish.\$bod);
28+
$(Craft.getCsrfInput()).appendTo(\$form);
29+
$('<input/>', {
30+
type: 'hidden',
31+
name: 'action',
32+
value: 'assets/download-asset'
33+
}).appendTo(\$form);
34+
selectedItems.each(function() {
35+
$('<input/>', {
36+
type: 'hidden',
37+
name: 'assetId[]',
38+
value: $(this).data('id')
39+
}).appendTo(\$form);
40+
});
41+
$('<input/>', {
42+
type: 'submit',
43+
value: 'Submit',
44+
}).appendTo(\$form);
45+
\$form.submit();
46+
\$form.remove();
47+
},
48+
});
49+
})();
50+
JS, [static::class]);
51+
52+
return null;
53+
}
54+
}

src/Asset/Actions/EditImage.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CraftCms\Cms\Asset\Actions;
6+
7+
use CraftCms\Cms\Element\Actions\ElementAction;
8+
use CraftCms\Cms\Support\Facades\HtmlStack;
9+
10+
use function CraftCms\Cms\t;
11+
12+
class EditImage extends ElementAction
13+
{
14+
public string $label;
15+
16+
public function __construct(array|object $config = [])
17+
{
18+
parent::__construct($config);
19+
20+
$this->label ??= t('Edit Image');
21+
}
22+
23+
#[\Override]
24+
public function getTriggerLabel(): string
25+
{
26+
return $this->label;
27+
}
28+
29+
public function getTriggerHtml(): ?string
30+
{
31+
HtmlStack::jsWithVars(fn ($type) => <<<JS
32+
(() => {
33+
new Craft.ElementActionTrigger({
34+
type: $type,
35+
bulk: false,
36+
validateSelection: (selectedItems, elementIndex) => Garnish.hasAttr(selectedItems.find('.element'), 'data-editable-image'),
37+
activate: (selectedItems, elementIndex) => {
38+
const \$element = selectedItems.find('.element:first');
39+
new Craft.AssetImageEditor(\$element.data('id'));
40+
},
41+
});
42+
})();
43+
JS, [static::class]);
44+
45+
return null;
46+
}
47+
}

0 commit comments

Comments
 (0)