Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/
- 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\*`, 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.
- Removed the element action classes that only had client-side behavior, including the legacy `craft\elements\actions\Copy`, `Edit`, `View`, `PreviewAsset`, `CopyUrl`, `CopyReferenceTag`, `DownloadAssetFile`, `EditImage`, `MoveAssets`, `RenameFile`, `ReplaceFile`, `ShowInFolder`, `MoveToSection`, `NewChild`, `NewSiblingBefore`, and `NewSiblingAfter` classes. `CraftCms\Cms\Element\Actions\ClientAction` objects should be used instead. ([#20272](https://github.com/craftcms/cms/pull/20272))
- Removed `getTriggerHtml()` and `getTriggerId()` from `CraftCms\Cms\Element\Contracts\ElementActionInterface` and `CraftCms\Cms\Element\Actions\ElementAction`. The legacy element index’s triggers for the built-in element actions are now provided by the Yii2 adapter, and `craft\base\ElementAction` still has both methods, for element actions that render their own trigger.

##### Validation
Craft 6 introduces a new validation system that uses Laravel's Validator instead of Yii2's model validation.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Removed `getTriggerHtml()` and `getTriggerId()` from `CraftCms\Cms\Element\Contracts\ElementActionInterface` and `CraftCms\Cms\Element\Actions\ElementAction`. The legacy element index’s triggers for the built-in element actions are now provided by the Yii2 adapter, and `craft\base\ElementAction` still has both methods, for element actions that render their own trigger.
- Fixed an error that occurred when changing a password from the **My Account** → **Password** screen. ([#20585](https://github.com/craftcms/cms/pull/20585))
- Fixed a bug where asset indexing included the default asset transformer’s volume directories. ([#20589](https://github.com/craftcms/cms/pull/20589))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/** global: $ */
/** global: jQuery */

import {createActionTriggers} from './action-triggers';

/**
* Element index class
*/
Expand Down Expand Up @@ -3850,6 +3852,7 @@ Craft.BaseElementIndex = Garnish.Base.extend(

await Craft.appendHeadHtml(this.actionsHeadHtml);
await Craft.appendBodyHtml(this.actionsBodyHtml);
createActionTriggers(this.actions);

Craft.initUiElements(this._$triggers);

Expand Down
168 changes: 168 additions & 0 deletions packages/craftcms-legacy/cpcompat/src/element-index/action-triggers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/** global: Craft */
/** global: Garnish */

/**
* Trigger settings for the built-in element actions, keyed by the
* `legacyTrigger.handler` the server sends with each action's config.
* Each factory receives the rest of `legacyTrigger`.
*/
const handlers = {
delete: ({withDescendants, hardDelete, confirmationMessage}) => ({
validateSelection: (selectedItems, elementIndex) =>
everyElement(selectedItems, (element) =>
Garnish.hasAttr(element, 'data-deletable')
) && elementIndex.settings.canDeleteElements(selectedItems),
activate: (selectedItems, elementIndex) =>
deleteElements(selectedItems, elementIndex, {
withDescendants,
hardDelete,
confirmationMessage,
}),
}),

deleteAssets: ({hardDelete, confirmationMessage}) => ({
requireId: false,
validateSelection: (selectedItems, elementIndex) =>
everyElement(selectedItems, (element) => {
if (!Garnish.hasAttr(element, 'data-is-folder')) {
return Garnish.hasAttr(element, 'data-deletable');
}

// Only one folder at a time
if (selectedItems.length !== 1) {
return false;
}

const sourcePath = element.data('source-path') || [];
return (
sourcePath.length !== 0 &&
!!sourcePath[sourcePath.length - 1].canDelete
);
}) && elementIndex.settings.canDeleteElements(selectedItems),
activate: async (selectedItems, elementIndex) => {
const element = selectedItems.find('.element:first');

if (Garnish.hasAttr(element, 'data-is-folder')) {
const sourcePath = element.data('source-path');
await elementIndex.deleteFolder(sourcePath[sourcePath.length - 1]);
elementIndex.updateElements();
return;
}

await deleteElements(selectedItems, elementIndex, {
hardDelete,
confirmationMessage,
});
},
}),

duplicate: ({attribute}) => ({
validateSelection: (selectedItems, elementIndex) =>
everyElement(selectedItems, (element) =>
Garnish.hasAttr(element, attribute)
) && elementIndex.settings.canDuplicateElements(selectedItems),
beforeActivate: async (selectedItems, elementIndex) => {
await elementIndex.onBeforeDuplicateElements(selectedItems);
},
afterActivate: async (selectedItems, elementIndex) => {
await elementIndex.onDuplicateElements(selectedItems);
},
}),

deleteForSite: () => ({
validateSelection: (selectedItems, elementIndex) =>
everyElement(selectedItems, (element) =>
Garnish.hasAttr(element, 'data-deletable-for-site')
) && elementIndex.settings.canDeleteElements(selectedItems),
}),

restore: ({attribute}) => ({
validateSelection: (selectedItems) =>
everyElement(selectedItems, (element) =>
Garnish.hasAttr(element, attribute)
),
}),

setStatus: () => ({
validateSelection: (selectedItems) =>
everyElement(
selectedItems,
(element) =>
Garnish.hasAttr(element, 'data-savable') &&
!Garnish.hasAttr(element, 'data-disallow-status')
),
}),

suspendUsers: ({currentUserId}) => ({
bulk: true,
validateSelection: (selectedItems) =>
everyElement(
selectedItems,
(element) =>
Garnish.hasAttr(element, 'data-can-suspend') &&
!Garnish.hasAttr(element, 'data-suspended') &&
element.data('id') != currentUserId
),
}),

unsuspendUsers: () => ({
bulk: true,
validateSelection: (selectedItems) =>
everyElement(
selectedItems,
(element) =>
Garnish.hasAttr(element, 'data-can-suspend') &&
Garnish.hasAttr(element, 'data-suspended')
),
}),
};

function everyElement(selectedItems, test) {
for (let i = 0; i < selectedItems.length; i++) {
if (!test(selectedItems.eq(i).find('.element'))) {
return false;
}
}

return true;
}

async function deleteElements(selectedItems, elementIndex, settings) {
await elementIndex.onBeforeDeleteElements(selectedItems);
elementIndex.setIndexBusy();
const elementIds = elementIndex.getSelectedElementIds();

new Craft.ElementDeletionManager(elementIndex.elementType, elementIds, {
siteId: elementIndex.siteId,
ownerId: elementIndex.settings.criteria?.ownerId,
...settings,
onLoadBlockers: () => {
elementIndex.setIndexAvailable();
},
onSuccess: async () => {
elementIndex.updateElements(true, true);
await elementIndex.onDeleteElements(selectedItems);
},
});
}

/**
* Creates the triggers for the actions that came with a built-in trigger.
*
* @param {Object[]} actions
*/
export function createActionTriggers(actions) {
for (const action of actions) {
const {handler, ...params} = action.legacyTrigger ?? {};

if (!handlers[handler]) {
continue;
}

new Craft.ElementActionTrigger({
type: action.type,
triggerId: action.triggerId,
...handlers[handler](params),
});
}
}
3 changes: 1 addition & 2 deletions resources/js/modules/element-action-trigger/compat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {ElementActionTrigger} from './element-action-trigger';
import {registerCraftGlobals} from '@/common/craft-global';

// `new Craft.ElementActionTrigger(...)` is emitted by PHP element actions and
// created by the (still-legacy) element index. Plain modern ES class.
// Created by the legacy element index, and by plugin actions' trigger JS.
registerCraftGlobals({ElementActionTrigger});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ declare const Craft: any;
* ElementActionTrigger — a port of `Craft.ElementActionTrigger` onto
* `@craftcms/garnish` `Base`. Wires an element-index action button: it
* enables/disables based on the current selection and runs the action's
* `activate` handler. Booted by PHP element actions (e.g. ShowInFolder) and by
* the (still-legacy) element index, so exposed on `window.Craft`.
* `activate` handler. Created by the legacy element index and by plugin
* actions' trigger JS, so exposed on `window.Craft`.
*/
export class ElementActionTrigger extends Base {
static defaults: any = {
Expand Down
77 changes: 1 addition & 76 deletions src/Asset/Actions/DeleteAssets.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,83 +5,8 @@
namespace CraftCms\Cms\Asset\Actions;

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

/**
* @since 6.0.0
*/
class DeleteAssets extends Delete
{
#[\Override]
public function getTriggerHtml(): ?string
{
// Only enable for deletable elements, per canDelete()
HtmlStack::jsWithVars(fn (
$triggerId,
$elementType,
$hardDelete,
$confirmationMessage,
) => <<<JS
(() => {
new Craft.ElementActionTrigger({
triggerId: $triggerId,
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 elementIndex.settings.canDeleteElements(selectedItems);
},
activate: async (selectedItems, elementIndex) => {
const element = selectedItems.find('.element:first');
if (Garnish.hasAttr(element, 'data-is-folder')) {
const sourcePath = element.data('source-path');
await elementIndex.deleteFolder(sourcePath[sourcePath.length - 1]);
elementIndex.updateElements();
} else {
await elementIndex.onBeforeDeleteElements(selectedItems);
elementIndex.setIndexBusy();
const elementIds = elementIndex.getSelectedElementIds();

new Craft.ElementDeletionManager($elementType, elementIds, {
siteId: elementIndex.siteId,
ownerId: elementIndex.settings.criteria?.ownerId,
hardDelete: $hardDelete,
confirmationMessage: $confirmationMessage,
onLoadBlockers: () => {
elementIndex.setIndexAvailable();
},
onSuccess: async () => {
elementIndex.updateElements(true, true);
await elementIndex.onDeleteElements(selectedItems);
},
});
}
},
});
})();
JS, [
$this->getTriggerId(),
$this->elementType,
$this->hard,
$this->confirmationMessage,
]);

return null;
}
}
class DeleteAssets extends Delete {}
71 changes: 0 additions & 71 deletions src/Element/Actions/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Support\Facades\Drafts;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\HtmlStack;
use CraftCms\Cms\Support\Facades\Workflows;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
Expand All @@ -32,21 +31,6 @@ class Delete extends ElementAction implements DeleteActionInterface

public ?string $successMessage = null;

private readonly string $triggerId;

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

$this->triggerId = sprintf('action-trigger-%s', mt_rand());
}

#[Override]
public function getTriggerId(): string
{
return $this->triggerId;
}

public function canHardDelete(): bool
{
return ! $this->withDescendants;
Expand All @@ -57,61 +41,6 @@ public function setHardDelete(): void
$this->hard = true;
}

public function getTriggerHtml(): ?string
{
// Only enable for deletable elements, per canDelete()
HtmlStack::jsWithVars(fn (
$triggerId,
$elementType,
$withDescendants,
$hardDelete,
$confirmationMessage,
) => <<<JS
(() => {
new Craft.ElementActionTrigger({
triggerId: $triggerId,
validateSelection: (selectedItems, elementIndex) => {
for (let i = 0; i < selectedItems.length; i++) {
if (!Garnish.hasAttr(selectedItems.eq(i).find('.element'), 'data-deletable')) {
return false;
}
}

return elementIndex.settings.canDeleteElements(selectedItems);
},
activate: async (selectedItems, elementIndex) => {
await elementIndex.onBeforeDeleteElements(selectedItems);
elementIndex.setIndexBusy();
const elementIds = elementIndex.getSelectedElementIds();

new Craft.ElementDeletionManager($elementType, elementIds, {
siteId: elementIndex.siteId,
ownerId: elementIndex.settings.criteria?.ownerId,
withDescendants: $withDescendants,
hardDelete: $hardDelete,
confirmationMessage: $confirmationMessage,
onLoadBlockers: () => {
elementIndex.setIndexAvailable();
},
onSuccess: async () => {
elementIndex.updateElements(true, true);
await elementIndex.onDeleteElements(selectedItems);
},
})
},
})
})();
JS, [
$this->getTriggerId(),
$this->elementType,
$this->withDescendants,
$this->hard,
$this->confirmationMessage,
]);

return null;
}

#[Override]
public function getTriggerLabel(): string
{
Expand Down
Loading
Loading