Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,14 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/
- Added `CraftCms\Cms\Element\ElementActivity`, `CraftCms\Cms\Element\Data\ElementActivity`, `CraftCms\Cms\Element\Enums\ElementActivityType`, and `CraftCms\Cms\Support\Facades\ElementActivity`.
- Deprecated `craft\services\Elements::getRecentActivity()`. `CraftCms\Cms\Element\ElementActivity::getRecentActivity()` should be used instead.
- Deprecated `craft\services\Elements::trackActivity()`. `CraftCms\Cms\Element\ElementActivity::trackActivity()` should be used instead.
- 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`.
- Added Laravel-native element action classes under `CraftCms\Cms\Element\Actions`, `CraftCms\Cms\Asset\Actions`, `CraftCms\Cms\Entry\Actions`, and `CraftCms\Cms\User\Actions`.
- 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`.
- Added Laravel-native element exporter classes under `CraftCms\Cms\Element\Exporters`.
- Deprecated `craft\errors\InvalidTypeException`. `CraftCms\Cms\Element\Exceptions\InvalidTypeException` should be used instead.
- Deprecated `craft\errors\UnsupportedSiteException`. `CraftCms\Cms\Element\Exceptions\UnsupportedSiteException` should be used instead.
- 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.
- 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.

### Validation

Expand Down
6 changes: 6 additions & 0 deletions routes/actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
use CraftCms\Cms\Http\Controllers\Dashboard\Widgets\NewUsersController;
use CraftCms\Cms\Http\Controllers\Dashboard\WidgetsController;
use CraftCms\Cms\Http\Controllers\EditionController;
use CraftCms\Cms\Http\Controllers\Elements\ExportElementIndexController;
use CraftCms\Cms\Http\Controllers\Elements\PerformElementActionController;
use CraftCms\Cms\Http\Controllers\Entries\CreateEntryController;
use CraftCms\Cms\Http\Controllers\Entries\MoveEntryToSectionController;
use CraftCms\Cms\Http\Controllers\Entries\StoreEntryController;
Expand Down Expand Up @@ -207,6 +209,10 @@
Route::post('app/switch-to-licensed-edition', [EditionController::class, 'switchToLicensedEdition']);
});

// Elements
Route::post('element-indexes/export', ExportElementIndexController::class);
Route::post('element-indexes/perform-action', PerformElementActionController::class);

// Entries
Route::post('entries/create', CreateEntryController::class);
Route::post('entries/save-entry', StoreEntryController::class);
Expand Down
2 changes: 1 addition & 1 deletion src/Address/Elements/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
use Craft;
use craft\base\NestedElementInterface;
use craft\base\NestedElementTrait;
use craft\elements\actions\Copy;
use craft\elements\conditions\addresses\AddressCondition;
use CraftCms\Cms\Address\Addresses;
use CraftCms\Cms\Address\Models\Address as AddressModel;
use CraftCms\Cms\Address\Validation\AddressRules;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Actions\Copy;
use CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface;
use CraftCms\Cms\Element\Element;
use CraftCms\Cms\Element\Queries\AddressQuery;
Expand Down
45 changes: 45 additions & 0 deletions src/Asset/Actions/CopyReferenceTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Asset\Actions;

use CraftCms\Cms\Element\Actions\ElementAction;
use CraftCms\Cms\Support\Facades\HtmlStack;
use RuntimeException;

use function CraftCms\Cms\t;

class CopyReferenceTag extends ElementAction
{
#[\Override]
public function getTriggerLabel(): string
{
return t('Copy reference tag');
}

public function getTriggerHtml(): ?string
{
$refHandle = $this->elementType::refHandle();
if ($refHandle === null) {
throw new RuntimeException("Element type \"$this->elementType\" doesn't have a reference handle.");
}

HtmlStack::jsWithVars(fn ($type, $refHandle) => <<<JS
(() => {
new Craft.ElementActionTrigger({
type: $type,
bulk: false,
activate: (selectedItems, elementIndex) => {
Craft.ui.createCopyTextPrompt({
label: Craft.t('app', 'Copy the reference tag'),
value: '{' + $refHandle + ':' + selectedItems.find('.element').data('id') + '}',
});
},
})
})();
JS, [static::class, $refHandle]);

return null;
}
}
40 changes: 40 additions & 0 deletions src/Asset/Actions/CopyUrl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Asset\Actions;

use CraftCms\Cms\Element\Actions\ElementAction;
use CraftCms\Cms\Support\Facades\HtmlStack;

use function CraftCms\Cms\t;

class CopyUrl extends ElementAction
{
#[\Override]
public function getTriggerLabel(): string
{
return t('Copy URL');
}

public function getTriggerHtml(): ?string
{
HtmlStack::jsWithVars(fn ($type) => <<<JS
(() => {
new Craft.ElementActionTrigger({
type: $type,
bulk: false,
validateSelection: (selectedItems, elementIndex) => !!selectedItems.find('.element').data('url'),
activate: (selectedItems, elementIndex) => {
Craft.ui.createCopyTextPrompt({
label: Craft.t('app', 'Copy the URL'),
value: selectedItems.find('.element').data('url'),
});
},
})
})();
JS, [static::class]);

return null;
}
}
61 changes: 61 additions & 0 deletions src/Asset/Actions/DeleteAssets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Asset\Actions;

use CraftCms\Cms\Element\Actions\Delete;
use CraftCms\Cms\Support\Facades\HtmlStack;

class DeleteAssets extends Delete
{
#[\Override]
public function getTriggerHtml(): ?string
{
// Only enable for deletable elements, per canDelete()
HtmlStack::jsWithVars(fn ($type) => <<<JS
(() => {
const trigger = new Craft.ElementActionTrigger({
type: $type,
requireId: false,
validateSelection: (selectedItems, elementIndex) => {
for (let i = 0; i < selectedItems.length; i++) {
const element = selectedItems.eq(i).find('.element');
if (Garnish.hasAttr(element, 'data-is-folder')) {
if (selectedItems.length !== 1) {
// only one folder at a time
return false;
}
const sourcePath = element.data('source-path') || [];
if (!sourcePath.length || !sourcePath[sourcePath.length - 1].canDelete) {
return false;
}
} else {
if (!Garnish.hasAttr(element, 'data-deletable')) {
return false;
}
}
}

return true;
},

activate: (selectedItems, elementIndex) => {
const element = selectedItems.find('.element:first');
if (Garnish.hasAttr(element, 'data-is-folder')) {
const sourcePath = element.data('source-path');
elementIndex.deleteFolder(sourcePath[sourcePath.length - 1])
.then(() => {
elementIndex.updateElements();
});
} else {
elementIndex.submitAction(trigger.\$trigger.data('action'), Garnish.getPostData(trigger.\$trigger));
}
},
});
})();
JS, [static::class]);

return null;
}
}
54 changes: 54 additions & 0 deletions src/Asset/Actions/DownloadAssetFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Asset\Actions;

use CraftCms\Cms\Element\Actions\ElementAction;
use CraftCms\Cms\Support\Facades\HtmlStack;

use function CraftCms\Cms\t;

class DownloadAssetFile extends ElementAction
{
#[\Override]
public function getTriggerLabel(): string
{
return t('Download');
}

public function getTriggerHtml(): ?string
{
HtmlStack::jsWithVars(fn ($type) => <<<JS
(() => {
new Craft.ElementActionTrigger({
type: $type,
activate: (selectedItems, elementIndex) => {
var \$form = Craft.createForm().appendTo(Garnish.\$bod);
$(Craft.getCsrfInput()).appendTo(\$form);
$('<input/>', {
type: 'hidden',
name: 'action',
value: 'assets/download-asset'
}).appendTo(\$form);
selectedItems.each(function() {
$('<input/>', {
type: 'hidden',
name: 'assetId[]',
value: $(this).data('id')
}).appendTo(\$form);
});
$('<input/>', {
type: 'submit',
value: 'Submit',
}).appendTo(\$form);
\$form.submit();
\$form.remove();
},
});
})();
JS, [static::class]);

return null;
}
}
47 changes: 47 additions & 0 deletions src/Asset/Actions/EditImage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Asset\Actions;

use CraftCms\Cms\Element\Actions\ElementAction;
use CraftCms\Cms\Support\Facades\HtmlStack;

use function CraftCms\Cms\t;

class EditImage extends ElementAction
{
public string $label;

public function __construct(array|object $config = [])
{
parent::__construct($config);

$this->label ??= t('Edit Image');
}

#[\Override]
public function getTriggerLabel(): string
{
return $this->label;
}

public function getTriggerHtml(): ?string
{
HtmlStack::jsWithVars(fn ($type) => <<<JS
(() => {
new Craft.ElementActionTrigger({
type: $type,
bulk: false,
validateSelection: (selectedItems, elementIndex) => Garnish.hasAttr(selectedItems.find('.element'), 'data-editable-image'),
activate: (selectedItems, elementIndex) => {
const \$element = selectedItems.find('.element:first');
new Craft.AssetImageEditor(\$element.data('id'));
},
});
})();
JS, [static::class]);

return null;
}
}
Loading
Loading