diff --git a/resources/js/components/actions/ItemActions.vue b/resources/js/components/actions/ItemActions.vue
index 76d11472b92..1c9e688815e 100644
--- a/resources/js/components/actions/ItemActions.vue
+++ b/resources/js/components/actions/ItemActions.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/resources/js/pages/collections/Show.vue b/resources/js/pages/collections/Show.vue
index 68372b0174f..e6cbb162d62 100644
--- a/resources/js/pages/collections/Show.vue
+++ b/resources/js/pages/collections/Show.vue
@@ -135,25 +135,100 @@
/>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -191,7 +266,7 @@ import DeleteLocalizationConfirmation from '@/components/collections/DeleteLocal
import CollectionCalendar from '@/components/entries/calendar/Calendar.vue';
import SiteSelector from '@/components/SiteSelector.vue';
import { defineAsyncComponent } from 'vue';
-import { Dropdown, DropdownItem, DropdownLabel, DropdownMenu, DropdownSeparator, Header, Button, ToggleGroup, ToggleItem } from '@/components/ui';
+import { Dropdown, DropdownItem, DropdownLabel, DropdownMenu, DropdownSeparator, Header, Button, Skeleton, ToggleGroup, ToggleItem } from '@/components/ui';
import ItemActions from '@/components/actions/ItemActions.vue';
import Head from '@/pages/layout/Head.vue';
import { router } from '@inertiajs/vue3';
@@ -207,6 +282,7 @@ export default {
Dropdown,
Header,
Button,
+ Skeleton,
ToggleGroup,
ToggleItem,
PageTree: defineAsyncComponent(() => import('@/components/structures/PageTree.vue')),
@@ -398,6 +474,32 @@ export default {
return branch.redirect != null;
},
+ branchTreeActions(actions) {
+ // Destructive actions (e.g. Delete, DeleteMultisiteEntry) are excluded, since
+ // deleting from the tree is handled by the tree-aware "Delete" item instead.
+ return (actions || []).filter((action) => !action.dangerous);
+ },
+
+ treeActionStarted() {
+ Statamic.$progress.loading('action', true);
+ },
+
+ treeActionCompleted(successful = null, response = {}) {
+ Statamic.$progress.loading('action', false);
+
+ if (!successful) {
+ Statamic.$toast.error(response.message || __('Action failed'));
+ return;
+ }
+
+ if (response.message !== false) {
+ Statamic.$toast.success(response.message || __('Action completed'));
+ }
+
+ // Reloading the tree would discard any unsaved structure changes.
+ if (!this.treeIsDirty) this.$refs.tree?.refresh();
+ },
+
createEntry(blueprint, parent) {
let url = `${this.createUrl}?blueprint=${blueprint}`;
if (parent) url += '&parent=' + parent;
diff --git a/src/Structures/TreeBuilder.php b/src/Structures/TreeBuilder.php
index c6ebac6b2a8..d6155da3672 100644
--- a/src/Structures/TreeBuilder.php
+++ b/src/Structures/TreeBuilder.php
@@ -91,22 +91,23 @@ protected function transformTreeForController($tree)
$page = $item['page'];
$collection = $page->mountedCollection();
$referenceExists = $page->referenceExists();
+ $entry = $referenceExists ? $page->entry() : null;
return [
'id' => $page->id(),
'entry' => $page->reference(),
'title' => $page->hasCustomTitle() ? $page->title() : null,
- 'entry_title' => $referenceExists ? $page->entry()->value('title') : null,
- 'entry_blueprint' => $referenceExists ? [
- 'handle' => $page->entry()->blueprint()->handle(),
- 'title' => $page->entry()->blueprint()->title(),
+ 'entry_title' => $entry ? $entry->value('title') : null,
+ 'entry_blueprint' => $entry ? [
+ 'handle' => $entry->blueprint()->handle(),
+ 'title' => $entry->blueprint()->title(),
] : null,
'url' => $page->url(),
'edit_url' => $page->editUrl(),
- 'can_delete' => $referenceExists ? User::current()->can('delete', $page->entry()) : true,
+ 'can_delete' => $entry ? User::current()->can('delete', $entry) : true,
'slug' => $page->slug(),
'status' => $referenceExists ? $page->status() : null,
- 'redirect' => $referenceExists ? $page->entry()->get('redirect') : null,
+ 'redirect' => $entry ? $entry->get('redirect') : null,
'collection' => ! $collection ? null : [
'handle' => $collection->handle(),
'title' => $collection->title(),