Skip to content

[6.x] Make actions available in a collection tree view - #14331

Open
Devsome wants to merge 49 commits into
statamic:6.xfrom
Devsome:6.x
Open

Devsome wants to merge 49 commits into
statamic:6.xfrom
Devsome:6.x

Conversation

@Devsome

@Devsome Devsome commented Mar 24, 2026

Copy link
Copy Markdown

Hello everyone,

First of all, thank you for Statamic 6. After someone on Discord pointed me to Issue 575 and I saw the other pull requests 4070 & 4439, I decided to give it a try and follow the contribution guidelines.

Procedure:

  • Created a new blank project
  • Forked Statamic 6 and linked it locally in composer.json
  • Ran npm ci && npm run build in the statamic/cms project
  • Ran ddev artisan vendor:publish --tag=statamic-cp --force && ddev yarn build && ddev artisan optimize:clear
  • Created a custom action ddev artisan statamic:make:action

If I've forgotten anything or something is missing, please let me know. Even if it isn't accepted, as stated in the Contribution Guidelines, it might still be helpful to others who need it.

This is what my custom action looks like

<?php

namespace App\Actions;

use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Statamic\Actions\Action;

class CustomActionInTreeView extends Action
{
    protected $icon = 'add-item';

    /**
     * The run method
     *
     * @return mixed
     */
    public function run($items, $values)
    {
        Log::info('Custom Action in Tree View executed', [
            'items' => $items->map->id()->all(),
            'values' => $values,
        ]);

        if (($values['outcome'] ?? 'success') === 'fail') {
            // A plain Exception is caught by the ActionController and returned with HTTP 200
            // ({ success: false }), which the frontend still treats as successful.
            // A ValidationException results in a 422, which triggers the "failed" path.
            throw ValidationException::withMessages([
                'outcome' => 'Custom Action in Tree View failed (simulated).',
            ]);
        }

        return 'Custom Action in Tree View completed for '.$items->count().' item(s).';
    }

    protected function fieldItems()
    {
        return [
            'outcome' => [
                'type' => 'button_group',
                'display' => 'Outcome',
                'instructions' => 'Choose which result the action should simulate.',
                'options' => [
                    'success' => 'Success',
                    'fail' => 'Fail',
                ],
                'default' => 'success',
            ],
        ];
    }

    public static function title(): string
    {
        return 'Custom Action in Tree View';
    }

    public function visibleTo($item): bool
    {
        $view = $this->context['view'] ?? 'list';

        return $view === 'tree';
    }
}

How it looks like in the TreeView:
image

How it looks like in the ListView:
image

@Devsome Devsome changed the title Make actions available in a collection tree view [6.x] Make actions available in a collection tree view Mar 24, 2026

@jasonvarga jasonvarga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, thanks!

However we are moving away from sending all the actions down the wire initially. It really bloats the responses.

We have a mechanism to lazily load the actions when you hover over the dropdown. You should rework this PR to allow for that.

See RowActions.vue which is used in the Listing component.

https://github.com/statamic/cms/blob/6.x/resources/js/components/ui/Listing/RowActions.vue

In fact you might actually be able to use the RowActions component itself.

Devsome added 2 commits April 7, 2026 09:40
…e component is mounted. Update the Show page to display the skeleton loader. Remove unnecessary action references in TreeBuilder.
@Devsome

Devsome commented Apr 7, 2026

Copy link
Copy Markdown
Author

Hello @jasonvarga thanks for your response,
I've made some adjustments based on what I understood from you.
My „CustomActionInTreeView“ which I mentioned in my first comment, still works as intended.

Feel free to let me know what you think.
Best regards

@Devsome
Devsome requested a review from jasonvarga April 7, 2026 08:06
@Devsome

Devsome commented Apr 14, 2026

Copy link
Copy Markdown
Author

@jasonvarga I also fixed the issue where the modal remained open after clicking an action, and based on what I saw in RowActions.vue, I’ve now set the „default“ value for :variants in Show.vue instead of undefined.

@Devsome
Devsome marked this pull request as draft April 15, 2026 08:03
@Devsome
Devsome marked this pull request as ready for review April 15, 2026 12:45
@Devsome

Devsome commented Apr 15, 2026

Copy link
Copy Markdown
Author

I've made a few more adjustments to ensure it runs smoothly. Please give it a try and let me know what you think.

@Devsome

Devsome commented Jun 17, 2026

Copy link
Copy Markdown
Author

@jasonvarga Is there anything I can do to help here, or are you guys just too busy with other, more important PR right now? Cheers

@Devsome

Devsome commented Jul 28, 2026

Copy link
Copy Markdown
Author

dunno what happend, but the PHPStan / Analyze (pull_request) test is failing.

@duncanmcclean

Copy link
Copy Markdown
Member

Don't worry about it - #15083 should fix it.

@jasonvarga jasonvarga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was generated by an AI (Claude), performing an automated code review pass.

Three issues found that should be addressed before merge:

  1. Tree actions run with no success/failure feedback and no tree refresh.
  2. DeleteMultisiteEntry slips past the delete-handle filter and remains a second, non-tree-aware delete path in multi-site collections.
  3. The second DropdownSeparator can render orphaned when neither built-in menu item is present.

See inline comments for details and suggested fixes.

Comment on lines +138 to +144
<ItemActions
v-if="branch.entry"
:url="entriesActionUrl"
:context="{ view: 'tree' }"
:item="branch.entry"
v-slot="{ actions, loadActions, shouldShowSkeleton }"
>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning — Actions run silently: no success/failure feedback, no tree refresh

ItemActions emits started/completed events, and the sibling RowActions.vue (used by the standard list view) listens for these to show a toast and refresh:

function actionSuccess(response) {
    if (response.message !== false) Statamic.$toast.success(response.message || __('Action completed'));
    refresh();
}
function actionFailed(response) {
    Statamic.$toast.error(response.message || __('Action failed'));
}

runServerAction/handleActionSuccess (resources/js/components/actions/Actions.js) never show a toast themselves — that's entirely the calling component's job via completed. This usage binds neither @started nor @completed. Running any real action from the tree dropdown gives no confirmation toast, no error message, and if the action mutates the entry, the tree won't reflect it until a manual reload. Worth wiring @started/@completed the way RowActions.vue does, before merge.

Comment thread resources/js/pages/collections/Show.vue Outdated
@click="deleteTreeBranch(branch, removeBranch)"
/>

<DropdownSeparator v-if="shouldShowSkeleton || branchTreeActions(actions).length" />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning — Orphaned separator when neither built-in dropdown option is present

When both built-in groups are absent (depth >= structureMaxDepth AND !branch.can_delete), this separator still renders above the actions list with nothing above it — an orphaned separator at the top of the menu.

Fix:

<DropdownSeparator v-if="(depth < structureMaxDepth || branch.can_delete) && (shouldShowSkeleton || branchTreeActions(actions).length)" />

Comment thread resources/js/pages/collections/Show.vue Outdated
},

branchTreeActions(actions) {
return (actions || []).filter((action) => action.handle !== 'delete');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning — DeleteMultisiteEntry still leaks into multi-site tree dropdowns as a second, non-tree-aware delete path

DeleteMultisiteEntry extends Delete (src/Actions/DeleteMultisiteEntry.php) and doesn't override handle(), so via HasHandle::handle() (Str::snake($shortClassName)) its handle is delete_multisite_entry, not delete — this filter doesn't catch it. Its visibleTo() only checks multi-site + non-root, with no awareness of context.view, so it stays visible in the tree.

Result: multi-site collection trees get two delete paths — the built-in "Delete" item (tree-aware, calls deleteTreeBranch(), deferred until "Save Changes") and DeleteMultisiteEntry via the actions list (runs immediately as a server action through ConfirmableAction, isn't tree-aware, and won't remove the node from the UI — leaves a ghost entry until refresh).

Fix: filter on !action.dangerous instead of enumerating handles, to also catch this and any future delete-like action:

branchTreeActions(actions) {
    return (actions || []).filter((action) => !action.dangerous);
},

@Devsome

Devsome commented Sep 15, 2026

Copy link
Copy Markdown
Author

Hello, and thank you, @jasonvarga, for the detailed response. I’ve looked into the issue and fixed the errors you (Claude) pointed out. I’ve also updated my CustomActionTreeView.php in the first post. If you don’t specify a return value in the public function run, the default return value is used. I’d appreciate any feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants